@genee/omp-opsx-addon 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/index.ts +184 -66
- package/lib/agent-defs.ts +40 -4
- package/lib/edit-variant-pin.ts +4 -4
- package/lib/logger.ts +33 -0
- package/lib/model-roles.ts +270 -0
- package/lib/pipe-core.ts +784 -0
- package/lib/pipe-push.ts +467 -0
- package/lib/reachability-cache.ts +2 -1
- package/lib/reachability-probe.ts +4 -3
- package/lib/status-probe.ts +289 -0
- package/lib/system-prompt.ts +4 -4
- package/lib/unified-config.ts +48 -1
- package/lib/usage-poller.ts +208 -7
- package/lib/usage-redis-multiproc.worker.ts +5 -1
- package/lib/usage-render.ts +146 -95
- package/lib/usage-resolver.ts +10 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,6 +63,15 @@ tiers:
|
|
|
63
63
|
# 可选:role → tier 期望
|
|
64
64
|
role_tiers:
|
|
65
65
|
planner: top
|
|
66
|
+
|
|
67
|
+
# 可选:OMP 内置/自定义 model role → tier 期望(或 skip 排除)
|
|
68
|
+
# 仅影响 /pick-model 后 session 覆盖写入哪些模型;合法值 tiny/low/mid/high/top/skip
|
|
69
|
+
model_role_tiers:
|
|
70
|
+
advisor: skip
|
|
71
|
+
plan: skip
|
|
72
|
+
tiny: skip
|
|
73
|
+
vision: skip # vision 固定 high 档候选,仅 skip 有效(见下文)
|
|
74
|
+
task: low
|
|
66
75
|
```
|
|
67
76
|
|
|
68
77
|
---
|
|
@@ -73,6 +82,79 @@ role_tiers:
|
|
|
73
82
|
|
|
74
83
|
决策日志打印到 stderr:`[omp-opsx-addon] planner → anthropic/claude-opus-4-6 (tier=top, gap=0, remaining=0.80)`
|
|
75
84
|
|
|
85
|
+
|
|
86
|
+
### 子 agent 模型落点:session-scoped modelRoles
|
|
87
|
+
|
|
88
|
+
`/pick-model <selector>` / `refresh` / `reset` 应用选择结果时,插件**不**再按 agent 名写
|
|
89
|
+
`task.agentModelOverrides`,而是写 OMP session 运行时覆盖
|
|
90
|
+
`settings.overrideModelRoles({ …全 role… })`——覆盖**宿主当前已知的全部 model role**
|
|
91
|
+
(10 个内置 role + 自定义 role:`getKnownRoleIds` 从 cycleOrder/modelRoles/modelTags 汇总):
|
|
92
|
+
|
|
93
|
+
- 每个 role 跟随一个 tier 的选中模型;同一 tier 的多个 role 共用同一个模型(同候选集
|
|
94
|
+
排序结果唯一)。mid/high 复用 opsx 4 role 的选择(coder→mid、reviewer→high),
|
|
95
|
+
tiny/low/top 在同一过滤候选集上补选。
|
|
96
|
+
- **内置 role 默认 tier 映射**:
|
|
97
|
+
|
|
98
|
+
| role | tier | 触发场景 |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `tiny` | tiny | 极小任务 |
|
|
101
|
+
| `smol` | mid | scout 等快速任务 |
|
|
102
|
+
| `task` | mid | task 工具委派的全能力子 agent(与 scout 同档,防能力倒挂) |
|
|
103
|
+
| `commit` | low | AI commit |
|
|
104
|
+
| `default` | high | 主会话 / reviewer |
|
|
105
|
+
| `designer` | high | designer agent |
|
|
106
|
+
| `vision` | high | `/vision`、图像输入(**能力门控**,见下) |
|
|
107
|
+
| `slow` | top | 深度思考 |
|
|
108
|
+
| `plan` | top | plan mode |
|
|
109
|
+
| `advisor` | top | advisor 会话 |
|
|
110
|
+
|
|
111
|
+
**激活语义**:advisor/plan/tiny 等默认未配置的 role 被写入覆盖后,**只在对应功能真正
|
|
112
|
+
触发时**(plan mode、advisor 会话、auto-thinking、AI commit、`/vision`)才会解析到
|
|
113
|
+
selector 模型;功能不触发就不产生消耗。覆盖 session 结束即还原。
|
|
114
|
+
- **自定义 role**(cycleOrder/modelTags 里出现、不在内置表中的 role)默认跟随 `high`,
|
|
115
|
+
并一次性 warn 提示可用 `model_role_tiers` 调档或排除。
|
|
116
|
+
- **`model_role_tiers`(opsx.yml)**:OMP role 名 → `tiny/low/mid/high/top` 或 `skip`,
|
|
117
|
+
覆盖上表默认值;自定义 role 也可配。`skip` 是唯一逃生舱——该 role **不写** session
|
|
118
|
+
覆盖、保留你的 config 值(默认策略是全量覆盖,包含 config 里的 `modelRoles.*` 钉值,
|
|
119
|
+
不做「自动跳过钉值」)。**例外:`vision` role 固定使用 high 档候选**(仅在具备 image
|
|
120
|
+
能力的模型中挑选,见下条),`model_role_tiers.vision` **仅 `skip` 生效**——配
|
|
121
|
+
`top/low/…` 等其他 tier 值会被静默接受但**不改变其选档**(仍按 high 档选图像模型)。
|
|
122
|
+
保守配置示例:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
model_role_tiers:
|
|
126
|
+
advisor: skip # 不想让 advisor 会话走 selector 模型
|
|
127
|
+
plan: skip # plan mode 保留 config 钉值
|
|
128
|
+
tiny: skip # 无 tiny 档模型时避免回落
|
|
129
|
+
vision: skip # 不覆盖 vision(vision 固定 high 档选图像模型,仅 skip 有效)
|
|
130
|
+
task: low # 显式降档
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **vision 能力门控**:vision 候选**固定按 high 档**、只从声明 `input` 含 `image` 的模型
|
|
134
|
+
中挑选(不按 `model_role_tiers.vision` 的 tier 值选档,该键仅 `skip` 有效);当前选择里
|
|
135
|
+
没有图像模型时,vision role **不写覆盖**(warn 汇总为 `vision(capability)`,绝不写入
|
|
136
|
+
纯文本模型),vision 功能回落 config/default。恢复手段:换一个含图像模型的 selector
|
|
137
|
+
或用 `model_allowlist` 纳入图像模型;**`model_role_tiers.vision` 改 tier 绕不过门控、
|
|
138
|
+
也不改选档**。
|
|
139
|
+
- **auto role 的 agent 定义**在 `.omp/agents/*.md` frontmatter 注入带引号的 tier alias
|
|
140
|
+
(coder `model: "@smol"`,其余三者 `model: "@default"`);task 工具经 modelRoles 解析。
|
|
141
|
+
- 因此**所有经 role 解析的 agent 都跟随切换**——包括插件未枚举的 OMP 内置/bundled/
|
|
142
|
+
project agent(例如内置 scout 走 `smol`,`/pick-model` selector/refresh/reset 后
|
|
143
|
+
scout 自动换到选中模型)。
|
|
144
|
+
- auto role 与 omp-alias(如 `slow`,值无 `/`)的 dispatch 提示词**不传 `model=`**,
|
|
145
|
+
由 frontmatter alias 声明式接管;pinned 具体 provider/model 仍显式传 model,行为不变。
|
|
146
|
+
- 某 tier 补选没有可用模型(候选耗尽/allowlist 全过滤)时,该 tier 的各 role 不写覆盖、
|
|
147
|
+
warn 汇总(`no-pick`),命令不中断;无任何模型时计划为空,与既有「模型注册表不可用」
|
|
148
|
+
分支一致。
|
|
149
|
+
- 覆盖为 **session 作用域、不写盘**:`overrideModelRoles` 只写 runtime overlay,
|
|
150
|
+
session 结束即还原;不会修改你的 `~/.omp/agent/config.yml`。每次应用前先
|
|
151
|
+
`clearOverride('modelRoles')`,避免上一 selector 的旧 role 值残留;`reset` 重算无约束
|
|
152
|
+
默认并覆盖(**不清空**,否则回落 config 硬钉)。
|
|
153
|
+
|
|
154
|
+
> **手动清理建议**:若全局 `~/.omp/agent/config.yml` 里有 `modelRoles.smol` 钉在会耗尽的
|
|
155
|
+
> 套餐模型(scout 会一直打它直到被 session 覆盖压过),或 `task.agentModelOverrides` 残留
|
|
156
|
+
> 已删除的 `tester` 等 key,可手动删除这些钉值;插件运行期间会以 session 空串中和 per-agent
|
|
157
|
+
> 钉值,但不改动你的配置文件。
|
|
76
158
|
---
|
|
77
159
|
|
|
78
160
|
## 开发
|
package/index.ts
CHANGED
|
@@ -104,23 +104,37 @@ function handleSubagentEvent(payload: unknown, primarySessionId: string): void {
|
|
|
104
104
|
sessionUsageRecorder.record(canonicalizeProvider(provider), usage, subagentStreamKey);
|
|
105
105
|
} catch (err) {
|
|
106
106
|
// Silently fail to avoid breaking the plugin
|
|
107
|
-
|
|
107
|
+
getLogger().error('Failed to handle subagent event', { err });
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from '@oh-my-pi/pi-coding-agent';
|
|
110
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, Settings } from '@oh-my-pi/pi-coding-agent';
|
|
111
|
+
import { getKnownRoleIds, MODEL_ROLE_IDS } from '@oh-my-pi/pi-coding-agent/config/model-roles';
|
|
111
112
|
import type { Model, Api } from '@oh-my-pi/pi-catalog/types';
|
|
112
113
|
import type { UsageReport } from '@oh-my-pi/pi-ai';
|
|
113
114
|
import { createCachedScanner } from './lib/change-scanner.js';
|
|
114
115
|
import { installAgents } from './lib/agent-defs.js';
|
|
116
|
+
import {
|
|
117
|
+
applyRoleModelOverrides,
|
|
118
|
+
buildRoleOverridePlan,
|
|
119
|
+
dispatchModelHint,
|
|
120
|
+
frontmatterModel,
|
|
121
|
+
ROLE_TO_OMP_ALIAS,
|
|
122
|
+
OMP_ROLE_TO_TIER,
|
|
123
|
+
ROLE_CAPABILITY,
|
|
124
|
+
type SkippedRole,
|
|
125
|
+
} from './lib/model-roles.js';
|
|
115
126
|
import { buildStaticPrompt, buildDynamicPrompt } from './lib/system-prompt.js';
|
|
116
127
|
import { type ProviderHealth, canonicalizeProvider, listDetectableProviders, healthCacheKey } from './lib/usage-resolver.js';
|
|
117
128
|
import { readOpsxSettings, type ResolvedOpsxConfig, type RoleValue, OPSX_ROLES } from './lib/unified-config.js';
|
|
118
|
-
import { selectAllRoles, mergePlanProviders, type SelectorResult } from './lib/model-selector.js';
|
|
119
|
-
import { scoreModelWithProvider } from './lib/model-tiers.js';
|
|
129
|
+
import { selectAllRoles, selectModel, mergePlanProviders, type SelectorResult } from './lib/model-selector.js';
|
|
130
|
+
import { scoreModelWithProvider, type TierName } from './lib/model-tiers.js';
|
|
120
131
|
import { DIRECT_FETCHERS } from './lib/direct-fetchers.js';
|
|
121
132
|
import { createUsageWidget } from './lib/usage-widget.js';
|
|
122
133
|
import { renderUsageReports, type ConsumptionTrack } from './lib/usage-render.js';
|
|
123
134
|
import { markExhausted, markAvailable, getExhaustedProviders } from './lib/provider-status.js';
|
|
135
|
+
import { registerPipeCore } from './lib/pipe-core.js';
|
|
136
|
+
import { registerPipePush } from './lib/pipe-push.js';
|
|
137
|
+
import { scheduleStatusProbes } from './lib/status-probe.js';
|
|
124
138
|
import {
|
|
125
139
|
startSharedPoller,
|
|
126
140
|
getUnconfirmedProviders,
|
|
@@ -157,6 +171,7 @@ import { filterSelectableModels, reachabilityFingerprint } from './lib/selection
|
|
|
157
171
|
import { parseAutoModelArgs, CHINA_EXCLUDE_FAMILIES, listFamilies, listProviders } from './lib/family-filter.js';
|
|
158
172
|
import { hasRateLimitError, hasRegionError, buildErrorDiagnosis, exclusionReasonStats } from './lib/error-scan.js';
|
|
159
173
|
import { applyEditVariantPin } from './lib/edit-variant-pin.js';
|
|
174
|
+
import { setOpsxLogger, getLogger } from './lib/logger.js';
|
|
160
175
|
type Role = 'coder' | 'reviewer' | 'planner' | 'proposal-reviewer';
|
|
161
176
|
|
|
162
177
|
/** Session-scoped selector constraint for `/pick-model <selector> [--china]`. */
|
|
@@ -265,24 +280,17 @@ export interface SelectionContext {
|
|
|
265
280
|
roleModels: Record<Role, string | undefined>;
|
|
266
281
|
/** Display label for each role from config, e.g. "auto", "smol", "deepseek-v4-pro". */
|
|
267
282
|
roleConfigs: Record<Role, string | undefined>;
|
|
283
|
+
/** Parsed role value kind for each role (auto | omp-alias | model | provider-model). */
|
|
284
|
+
roleKind: Record<Role, RoleValue['kind']>;
|
|
285
|
+
/** Full-role session overlay payload written to `overrideModelRoles` (all known OMP roles). */
|
|
286
|
+
roleOverrides: Record<string, string>;
|
|
287
|
+
/** Known roles left out of the overlay payload (config skip / capability gate / no pick). */
|
|
288
|
+
skippedRoles: SkippedRole[];
|
|
268
289
|
}
|
|
269
290
|
|
|
270
291
|
const DEFAULT_PLAN_PROVIDERS = new Set(['minimax-code-cn', 'opencode-go', 'zhipu-coding-plan', 'kimi', 'openai-codex']);
|
|
271
292
|
|
|
272
|
-
/** OPSX role → OMP role alias used in agent frontmatter `model: @<alias>`. */
|
|
273
|
-
const ROLE_TO_OMP_ALIAS: Record<Role, string> = {
|
|
274
|
-
coder: 'smol',
|
|
275
|
-
reviewer: 'default',
|
|
276
|
-
planner: 'default',
|
|
277
|
-
'proposal-reviewer': 'default',
|
|
278
|
-
};
|
|
279
293
|
|
|
280
|
-
/** OMP role alias → expected tier (without the `@`). */
|
|
281
|
-
const OMP_ROLE_TO_TIER: Record<string, string> = {
|
|
282
|
-
smol: 'mid',
|
|
283
|
-
default: 'high',
|
|
284
|
-
slow: 'top',
|
|
285
|
-
};
|
|
286
294
|
|
|
287
295
|
function roleValueToModelString(value: RoleValue): string | undefined {
|
|
288
296
|
if (value.kind === 'auto') return undefined;
|
|
@@ -313,19 +321,39 @@ function synthPinnedResult(role: string, modelString: string | undefined): Selec
|
|
|
313
321
|
export function buildDispatchPrompt(role: Role, task: string, selection: SelectionContext): string {
|
|
314
322
|
const agent = AGENT_MAP[role];
|
|
315
323
|
const sel = selection.selectorResults[role];
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
// auto roles (and omp-alias roles such as @slow) pass NO model: requestModel
|
|
325
|
+
// is the top-priority resolution layer and would shadow the frontmatter
|
|
326
|
+
// tier-role alias. Only concrete provider/model pins stay explicit.
|
|
327
|
+
const modelHint = dispatchModelHint(role, selection);
|
|
328
|
+
const parts = [`你的 opsx.${role} 模型由 role 自动解析。`];
|
|
318
329
|
if (modelHint) parts.push(`请通过 task 工具调用 ${agent} 并指定 model="${modelHint}"。`);
|
|
319
|
-
else parts.push(
|
|
330
|
+
else parts.push(`请直接通过 task 工具调用 ${agent}(无需指定 model,模型按 role 自动解析)。`);
|
|
320
331
|
if (sel?.decision) parts.push(`原因: ${sel.decision}`);
|
|
321
332
|
parts.push('直接用内置工具实现。');
|
|
322
333
|
return `请使用 task 工具委派给 ${agent}:\ntask(agent:"${agent}"${modelHint ? `, model:"${modelHint}"` : ''}, task:"${parts.join(' ')}\\n\\n${task}")`;
|
|
323
334
|
}
|
|
324
335
|
|
|
336
|
+
/**
|
|
337
|
+
* All model roles the host currently knows (10 built-in + custom roles from
|
|
338
|
+
* cycleOrder/modelRoles/modelTags). Host settings can throw during early
|
|
339
|
+
* startup or partial init; fall back to the built-in set rather than
|
|
340
|
+
* aborting selection (later recomputes pick up custom roles once ready).
|
|
341
|
+
*/
|
|
342
|
+
function safeGetKnownRoleIds(settings: Settings | null | undefined): string[] {
|
|
343
|
+
if (!settings) return [...MODEL_ROLE_IDS];
|
|
344
|
+
try {
|
|
345
|
+
return getKnownRoleIds(settings);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
getLogger().warn(`[omp-opsx-addon] getKnownRoleIds failed; using built-in model roles only: ${err instanceof Error ? err.message : String(err)}`);
|
|
348
|
+
return [...MODEL_ROLE_IDS];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
325
352
|
function computeSelectionSync(
|
|
326
353
|
config: ResolvedOpsxConfig,
|
|
327
354
|
models: Model<Api>[] | null,
|
|
328
355
|
health: Map<string, ProviderHealth> | null,
|
|
356
|
+
knownRoleIds: string[],
|
|
329
357
|
regionBlockedModels?: Set<string>,
|
|
330
358
|
unreachableProviders?: Set<string>,
|
|
331
359
|
availableByProvider?: Map<string, string[] | null>,
|
|
@@ -347,16 +375,23 @@ function computeSelectionSync(
|
|
|
347
375
|
const selectorResults = {} as Record<Role, SelectorResult>;
|
|
348
376
|
const roleModels = {} as Record<Role, string | undefined>;
|
|
349
377
|
const roleConfigs = {} as Record<Role, string | undefined>;
|
|
378
|
+
const roleKind = {} as Record<Role, RoleValue['kind']>;
|
|
350
379
|
const planSet = mergePlanProviders(config.planProviders);
|
|
351
380
|
for (const role of OPSX_ROLES) {
|
|
352
381
|
const value = config.roles[role];
|
|
353
|
-
|
|
382
|
+
roleKind[role] = value.kind;
|
|
383
|
+
// Config label shown in /pick-model choices markdown (`角色 (label)`):
|
|
384
|
+
// auto roles display the OMP tier-role alias; pinned roles the pin.
|
|
385
|
+
roleConfigs[role] = value.kind === 'auto'
|
|
386
|
+
? ROLE_TO_OMP_ALIAS[role]
|
|
387
|
+
: (roleValueToModelString(value) ?? '?');
|
|
354
388
|
const alias = `@${ROLE_TO_OMP_ALIAS[role]}`;
|
|
355
389
|
if (value.kind === 'auto') {
|
|
356
390
|
if (filteredModels && filteredModels.length > 0) {
|
|
357
391
|
const ompRole = ROLE_TO_OMP_ALIAS[role];
|
|
358
|
-
const tier = (config.roleTiers[role]
|
|
359
|
-
|
|
392
|
+
const tier = (config.roleTiers[role]
|
|
393
|
+
?? (OMP_ROLE_TO_TIER as Record<string, TierName>)[ompRole]
|
|
394
|
+
?? 'mid') as TierName;
|
|
360
395
|
const roleTiers = { ...config.roleTiers, [role]: tier };
|
|
361
396
|
const result = selectAllRoles(
|
|
362
397
|
[role],
|
|
@@ -387,9 +422,56 @@ function computeSelectionSync(
|
|
|
387
422
|
roleModels[role] = modelString?.includes('/') ? modelString : undefined;
|
|
388
423
|
}
|
|
389
424
|
}
|
|
390
|
-
|
|
425
|
+
// change: pick-model-batch-all-roles — build the full-role session overlay:
|
|
426
|
+
// every host-known role (10 built-in + custom) follows one tier's picked
|
|
427
|
+
// model. mid/high reuse the opsx picks (coder/reviewer); tiny/low/top are
|
|
428
|
+
// supplemental picks over the same filtered candidate set; vision picks
|
|
429
|
+
// from image-capable candidates only. See design D2/D3.
|
|
430
|
+
let roleOverrides: Record<string, string> = {};
|
|
431
|
+
let skippedRoles: SkippedRole[] = [];
|
|
432
|
+
if (filteredModels && filteredModels.length > 0) {
|
|
433
|
+
const pickString = (r: SelectorResult): string | undefined =>
|
|
434
|
+
r.picked ? `${r.picked.provider}/${r.picked.id}` : undefined;
|
|
435
|
+
const tierPicks: Partial<Record<TierName, string | undefined>> = {
|
|
436
|
+
mid: pickString(selectorResults.coder),
|
|
437
|
+
high: pickString(selectorResults.reviewer),
|
|
438
|
+
};
|
|
439
|
+
for (const supplementalTier of ['tiny', 'low', 'top'] as const) {
|
|
440
|
+
const synthRole = `__tier_${supplementalTier}`;
|
|
441
|
+
const result = selectModel({
|
|
442
|
+
role: synthRole,
|
|
443
|
+
models: filteredModels,
|
|
444
|
+
roleTiers: { [synthRole]: supplementalTier },
|
|
445
|
+
tierOverrides: config.tierOverrides,
|
|
446
|
+
health: health ?? undefined,
|
|
447
|
+
allowlist: config.modelAllowlist,
|
|
448
|
+
planProviders: planSet,
|
|
449
|
+
});
|
|
450
|
+
tierPicks[supplementalTier] = pickString(result);
|
|
451
|
+
}
|
|
452
|
+
const capabilityPicks: Record<string, string | undefined> = {};
|
|
453
|
+
const visionModels = filteredModels.filter((m) => ROLE_CAPABILITY.vision(m));
|
|
454
|
+
if (visionModels.length > 0) {
|
|
455
|
+
const visionResult = selectModel({
|
|
456
|
+
role: 'vision',
|
|
457
|
+
models: visionModels,
|
|
458
|
+
roleTiers: { vision: 'high' },
|
|
459
|
+
tierOverrides: config.tierOverrides,
|
|
460
|
+
health: health ?? undefined,
|
|
461
|
+
allowlist: config.modelAllowlist,
|
|
462
|
+
planProviders: planSet,
|
|
463
|
+
});
|
|
464
|
+
capabilityPicks.vision = pickString(visionResult);
|
|
465
|
+
}
|
|
466
|
+
const plan = buildRoleOverridePlan(
|
|
467
|
+
{ knownRoleIds, ompRoleTiers: config.ompRoleTiers, tierPicks, capabilityPicks },
|
|
468
|
+
(m) => getLogger().warn(m),
|
|
469
|
+
);
|
|
470
|
+
roleOverrides = plan.overrides;
|
|
471
|
+
skippedRoles = plan.skipped;
|
|
472
|
+
}
|
|
473
|
+
return { selectorResults, roleModels, roleConfigs, roleKind, roleOverrides, skippedRoles };
|
|
391
474
|
}
|
|
392
|
-
|
|
393
475
|
function formatModelChoicesMarkdown(sel: SelectionContext, suffix: string = '', primaryModel?: Model<Api>): string {
|
|
394
476
|
const rows: string[] = [];
|
|
395
477
|
let totalModelCount = 0;
|
|
@@ -480,6 +562,9 @@ const sessionUsageRecorder: SessionUsageRecorder = createSessionUsageRecorder({
|
|
|
480
562
|
// ── plugin entry ────────────────────────────────────────────────────
|
|
481
563
|
|
|
482
564
|
export default (pi: ExtensionAPI) => {
|
|
565
|
+
// Route plugin logging through the host logger (rolling file) instead of
|
|
566
|
+
// console.*, which would corrupt the TUI. Injected before any log path runs.
|
|
567
|
+
setOpsxLogger(pi.logger);
|
|
483
568
|
// Runtime edit-variant pin for deepseek-v4-flash. The host hard-maps the
|
|
484
569
|
// model to the sloppy §/» marker grammar (it misreads hashline ranges),
|
|
485
570
|
// but the model repeatedly breaks sloppy control lines too — retry loops
|
|
@@ -491,7 +576,11 @@ export default (pi: ExtensionAPI) => {
|
|
|
491
576
|
// host reads the merged view directly, making the file equivalent.
|
|
492
577
|
applyEditVariantPin(pi, { 'deepseek-v4-flash': 'replace' });
|
|
493
578
|
const directory = process.cwd();
|
|
494
|
-
const warn = (msg: string) =>
|
|
579
|
+
const warn = (msg: string) => pi.logger.warn(msg);
|
|
580
|
+
// Roles known to the host at the moment of a (re)selection: computed per
|
|
581
|
+
// call so custom roles appearing after startup are covered on
|
|
582
|
+
// refresh/reset/selector/tick.
|
|
583
|
+
const currentKnownRoleIds = (): string[] => safeGetKnownRoleIds(pi.pi?.settings);
|
|
495
584
|
const opsxConfig = readOpsxSettings(directory, warn);
|
|
496
585
|
// Static orchestration rules — built once, reused every turn for cache stability.
|
|
497
586
|
const STATIC_PROMPT = buildStaticPrompt();
|
|
@@ -582,6 +671,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
582
671
|
effectiveConfig,
|
|
583
672
|
models,
|
|
584
673
|
health,
|
|
674
|
+
currentKnownRoleIds(),
|
|
585
675
|
regionBlockedModels,
|
|
586
676
|
reach.unreachable,
|
|
587
677
|
reach.availableByProvider,
|
|
@@ -595,30 +685,16 @@ export default (pi: ExtensionAPI) => {
|
|
|
595
685
|
});
|
|
596
686
|
return selectionPromise;
|
|
597
687
|
};
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
for (const role of OPSX_ROLES) {
|
|
609
|
-
const picked = sel.selectorResults[role]?.picked;
|
|
610
|
-
if (picked) {
|
|
611
|
-
overrides[AGENT_MAP[role]] = `${picked.provider}/${picked.id}`;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (Object.keys(overrides).length > 0 || force) {
|
|
615
|
-
settings.override('task.agentModelOverrides', overrides);
|
|
616
|
-
warn(
|
|
617
|
-
`[omp-opsx-addon] task.agentModelOverrides: ${Object.entries(overrides)
|
|
618
|
-
.map(([k, v]) => `${k}=${v}`)
|
|
619
|
-
.join(', ')}`,
|
|
620
|
-
);
|
|
621
|
-
}
|
|
688
|
+
// Apply selector picks by TIER to session-scoped OMP modelRoles (smol =
|
|
689
|
+
// coder/mid, default = reviewer/high), so every role-resolving agent —
|
|
690
|
+
// including built-in scout (smol) — follows selector/refresh/reset.
|
|
691
|
+
// Persisted per-agent pins are neutralized at runtime with empty strings.
|
|
692
|
+
// pi.pi.settings is the host-process Settings singleton (the require()'d
|
|
693
|
+
// copy in node_modules is a separate, uninitialized instance — writing to
|
|
694
|
+
// it was a no-op). This thin wrapper is the single call site the
|
|
695
|
+
// constrained-period tick-reselect path (auto-model-family) also uses.
|
|
696
|
+
const applyRoleModel = (sel: SelectionContext): void => {
|
|
697
|
+
applyRoleModelOverrides(pi.pi?.settings, sel, warn);
|
|
622
698
|
};
|
|
623
699
|
|
|
624
700
|
// ── shared usage poller + widget ──────────────────────────────
|
|
@@ -794,9 +870,37 @@ export default (pi: ExtensionAPI) => {
|
|
|
794
870
|
// provider's aggregated consumption RATE (all sessions, main +
|
|
795
871
|
// subagent via cache backflow), and re-renders the widget. It never
|
|
796
872
|
// recomputes a selection, never applies overrides, never setModel.
|
|
873
|
+
const cfg = buildEffectiveConfig();
|
|
797
874
|
const onTick = () => {
|
|
875
|
+
// Fire-and-forget status probe: do not block the tick.
|
|
876
|
+
const { placeholderIds, active } = resolveActiveProviders();
|
|
877
|
+
let models: { id: string; provider: string; baseUrl?: string }[] = [];
|
|
878
|
+
try {
|
|
879
|
+
if (ctx.modelRegistry && typeof ctx.modelRegistry.getAvailable === 'function') {
|
|
880
|
+
models = ctx.modelRegistry.getAvailable() as { id: string; provider: string; baseUrl?: string }[];
|
|
881
|
+
}
|
|
882
|
+
} catch {
|
|
883
|
+
if (ctx.modelRegistry && typeof ctx.modelRegistry.getAll === 'function') {
|
|
884
|
+
try {
|
|
885
|
+
models = ctx.modelRegistry.getAll() as { id: string; provider: string; baseUrl?: string }[];
|
|
886
|
+
} catch {
|
|
887
|
+
models = [];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
void scheduleStatusProbes({
|
|
892
|
+
placeholderIds,
|
|
893
|
+
models,
|
|
894
|
+
getApiKey,
|
|
895
|
+
config: {
|
|
896
|
+
enabled: cfg.status_probe_enabled,
|
|
897
|
+
ttlMs: cfg.probe_ttl_ms,
|
|
898
|
+
timeoutMs: cfg.probe_timeout_ms,
|
|
899
|
+
excluded: cfg.excluded_providers,
|
|
900
|
+
},
|
|
901
|
+
}).then((written) => { if (written > 0) render(); }).catch(() => {});
|
|
902
|
+
|
|
798
903
|
sessionUsageRecorder.flush();
|
|
799
|
-
const { active } = resolveActiveProviders();
|
|
800
904
|
// Waveform source = per-second consumption sequence (timestamped
|
|
801
905
|
// deltas aggregated across sessions). Sample EVERY elapsed second
|
|
802
906
|
// for EVERY active provider — zeros when a second had no
|
|
@@ -822,7 +926,6 @@ export default (pi: ExtensionAPI) => {
|
|
|
822
926
|
lastSampledSec = lastSec;
|
|
823
927
|
render();
|
|
824
928
|
};
|
|
825
|
-
const cfg = buildEffectiveConfig();
|
|
826
929
|
await startSharedPoller(auth, DIRECT_FETCHERS, getApiKey, {
|
|
827
930
|
onTick: claimsOwnership ? onTick : undefined,
|
|
828
931
|
ownsUi,
|
|
@@ -952,7 +1055,12 @@ export default (pi: ExtensionAPI) => {
|
|
|
952
1055
|
// This fallback call is intentionally NOT threaded with familyConstraint:
|
|
953
1056
|
// it prefers the cached `selection`, and the fallback passes models=null so
|
|
954
1057
|
// filterSelectableModels' family branch never runs — it is not a reselect path.
|
|
955
|
-
const sel = selection ?? computeSelectionSync(
|
|
1058
|
+
const sel = selection ?? computeSelectionSync(
|
|
1059
|
+
opsxConfig,
|
|
1060
|
+
null,
|
|
1061
|
+
null,
|
|
1062
|
+
safeGetKnownRoleIds(pi.pi?.settings),
|
|
1063
|
+
);
|
|
956
1064
|
const changes = await changeScanner.scanChanges(directory, 'doing');
|
|
957
1065
|
// Layered injection: STATIC_PROMPT is byte-identical every turn → prefix cache
|
|
958
1066
|
// hit for [omp-base + skills + static]. Only the dynamic tail re-prefills.
|
|
@@ -966,17 +1074,14 @@ export default (pi: ExtensionAPI) => {
|
|
|
966
1074
|
|
|
967
1075
|
pi.on('session_start', async (_event, ctx) => {
|
|
968
1076
|
try {
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
if (v.kind === 'auto') return undefined;
|
|
972
|
-
return roleValueToModelString(v);
|
|
973
|
-
};
|
|
1077
|
+
const frontmatter = (role: Role): string | undefined =>
|
|
1078
|
+
frontmatterModel(role, opsxConfig.roles[role]);
|
|
974
1079
|
await installAgents(
|
|
975
1080
|
directory,
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1081
|
+
frontmatter('coder'),
|
|
1082
|
+
frontmatter('reviewer'),
|
|
1083
|
+
frontmatter('planner'),
|
|
1084
|
+
frontmatter('proposal-reviewer'),
|
|
980
1085
|
);
|
|
981
1086
|
warn('[omp-opsx-addon] agents ready (pick-model idle until /pick-model refresh)');
|
|
982
1087
|
} catch (e) {
|
|
@@ -992,7 +1097,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
992
1097
|
const getApiKey = ctx.modelRegistry?.getApiKeyForProvider
|
|
993
1098
|
? (provider: string) => ctx.modelRegistry.getApiKeyForProvider(provider)
|
|
994
1099
|
: undefined;
|
|
995
|
-
//
|
|
1100
|
+
// Provider exhaustion seen on the response/tool path; mark it locally
|
|
996
1101
|
// without a usage fetcher — record it in the process-local binary status
|
|
997
1102
|
// so the placeholder column reads 耗尽 (see lib/provider-status.ts).
|
|
998
1103
|
markExhausted(canonical);
|
|
@@ -1091,6 +1196,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1091
1196
|
effectiveConfig,
|
|
1092
1197
|
models,
|
|
1093
1198
|
health,
|
|
1199
|
+
currentKnownRoleIds(),
|
|
1094
1200
|
regionBlockedModels,
|
|
1095
1201
|
reach.unreachable,
|
|
1096
1202
|
reach.availableByProvider,
|
|
@@ -1126,7 +1232,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1126
1232
|
}
|
|
1127
1233
|
}
|
|
1128
1234
|
selection = freshSel;
|
|
1129
|
-
|
|
1235
|
+
applyRoleModel(freshSel);
|
|
1130
1236
|
if (changes.length > 0) {
|
|
1131
1237
|
pi.sendMessage({ customType: 'pick-model', content: `✅ 模型已刷新:\n${changes.join('\n')}`, display: true }, { triggerTurn: false });
|
|
1132
1238
|
} else {
|
|
@@ -1153,6 +1259,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1153
1259
|
effectiveConfig,
|
|
1154
1260
|
models,
|
|
1155
1261
|
health,
|
|
1262
|
+
currentKnownRoleIds(),
|
|
1156
1263
|
regionBlockedModels,
|
|
1157
1264
|
reach.unreachable,
|
|
1158
1265
|
reach.availableByProvider,
|
|
@@ -1177,7 +1284,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1177
1284
|
if (newPrimary) await pi.setModel(newPrimary);
|
|
1178
1285
|
selection = sel;
|
|
1179
1286
|
selectionHealthKey = buildSelectionHealthKey(health, reach, familyConstraint);
|
|
1180
|
-
|
|
1287
|
+
applyRoleModel(sel);
|
|
1181
1288
|
pi.sendMessage({
|
|
1182
1289
|
customType: 'pick-model',
|
|
1183
1290
|
content: changes.length > 0
|
|
@@ -1237,6 +1344,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1237
1344
|
effectiveConfig,
|
|
1238
1345
|
models,
|
|
1239
1346
|
health,
|
|
1347
|
+
currentKnownRoleIds(),
|
|
1240
1348
|
regionBlockedModels,
|
|
1241
1349
|
reach.unreachable,
|
|
1242
1350
|
reach.availableByProvider,
|
|
@@ -1296,7 +1404,7 @@ export default (pi: ExtensionAPI) => {
|
|
|
1296
1404
|
if (newPrimary) await pi.setModel(newPrimary);
|
|
1297
1405
|
selection = sel;
|
|
1298
1406
|
selectionHealthKey = buildSelectionHealthKey(health, reach, familyConstraint);
|
|
1299
|
-
|
|
1407
|
+
applyRoleModel(sel);
|
|
1300
1408
|
const content = `✅ 已切换 selector 约束 (${formatFamilyConstraint(familyConstraint)}):\n${changes.join('\n')}${hint ? `\n${hint}` : ''}`;
|
|
1301
1409
|
pi.sendMessage({
|
|
1302
1410
|
customType: 'pick-model',
|
|
@@ -1351,4 +1459,14 @@ export default (pi: ExtensionAPI) => {
|
|
|
1351
1459
|
if (selection) return selection;
|
|
1352
1460
|
return ensureSelection(ctx);
|
|
1353
1461
|
}
|
|
1462
|
+
|
|
1463
|
+
// Cross-broker project-scoped pipe core: registers opsx_pipe_list /
|
|
1464
|
+
// _send / _recv / _wait and starts presence heartbeat + GC on
|
|
1465
|
+
// session_start (managed timers inside lib/pipe-core).
|
|
1466
|
+
registerPipeCore(pi);
|
|
1467
|
+
|
|
1468
|
+
// Cross-broker push/injector: watches the local mbox slot and injects
|
|
1469
|
+
// inbound pipe messages as custom (opsx-pipe) turns; /opsx-pipe mute
|
|
1470
|
+
// toggles the stop-injection flag (managed timers inside lib/pipe-push).
|
|
1471
|
+
registerPipePush(pi);
|
|
1354
1472
|
};
|
package/lib/agent-defs.ts
CHANGED
|
@@ -5,7 +5,7 @@ const MARKER = '<!-- @genee/omp-opsx-addon -->';
|
|
|
5
5
|
const CODE_REVIEWER_MD = (model?: string) => `---
|
|
6
6
|
name: code-reviewer
|
|
7
7
|
description: OpenSpec 代码审查 agent,审查代码实现并执行全局验证
|
|
8
|
-
tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
8
|
+
tools: read, bash, glob, grep${model ? `\nmodel: "${model}"` : ''}
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
你是一位资深代码审查员。你根据 OpenSpec 提案和设计文档审查代码实现,并在审阅的同时执行最后一次全局验证。
|
|
@@ -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 定义的内容?有无缺失或偏离?
|
|
@@ -65,7 +71,7 @@ ${MARKER}
|
|
|
65
71
|
const PROPOSAL_REVIEWER_MD = (model?: string) => `---
|
|
66
72
|
name: proposal-reviewer
|
|
67
73
|
description: OpenSpec 提案审查 agent,审查提案质量
|
|
68
|
-
tools: read, bash, glob, grep${model ? `\nmodel: ${model}` : ''}
|
|
74
|
+
tools: read, bash, glob, grep${model ? `\nmodel: "${model}"` : ''}
|
|
69
75
|
---
|
|
70
76
|
|
|
71
77
|
你是一位资深需求审查员。你审查 OpenSpec 提案的完整性和质量。
|
|
@@ -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 是否齐全?有无缺失章节?
|
|
@@ -112,7 +123,7 @@ ${MARKER}
|
|
|
112
123
|
const CODER_MD = (model?: string) => `---
|
|
113
124
|
name: coder
|
|
114
125
|
description: OpenSpec 变更实现 agent,同步 tasks.md 进度
|
|
115
|
-
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` : ''}
|
|
126
|
+
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: "${model}"` : ''}
|
|
116
127
|
skill: openspec-apply-change
|
|
117
128
|
---
|
|
118
129
|
|
|
@@ -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
|
---
|
|
@@ -159,7 +184,7 @@ ${MARKER}
|
|
|
159
184
|
const PLANNER_MD = (model?: string) => `---
|
|
160
185
|
name: planner
|
|
161
186
|
description: OpenSpec 提案规划 agent
|
|
162
|
-
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: ${model}` : ''}\nskill: openspec-propose
|
|
187
|
+
tools: read, write, edit, bash, glob, grep, todo${model ? `\nmodel: "${model}"` : ''}\nskill: openspec-propose
|
|
163
188
|
---
|
|
164
189
|
|
|
165
190
|
你是一个 OpenSpec 提案规划 agent。负责创建和更新 OpenSpec 变更提案文档。
|
|
@@ -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 确认后再开写。
|