@genee/omp-opsx-addon 0.7.0 → 0.9.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.
@@ -108,6 +108,15 @@ export const DEFAULT_RENDER_TICK_MS = 1_000;
108
108
  export const DEFAULT_MIN_FETCH_INTERVAL_MS = 10_000;
109
109
  /** Default idle fallback period (60s): providers with no consumption still re-fetch. */
110
110
  export const DEFAULT_IDLE_DIRTY_MS = 60_000;
111
+ /**
112
+ * Default wall-time budget for the cold-start refresh inside
113
+ * {@link startSharedPoller}. `session_start` awaits this function; if the
114
+ * initial network fetch hasn't settled within the budget it detaches to the
115
+ * background instead of pinning the extension handler (the host aborts event
116
+ * handlers at 30s). Disk-seeded data renders immediately and the 1s tick picks
117
+ * the fetch result up as soon as it lands.
118
+ */
119
+ export const DEFAULT_STARTUP_FETCH_BUDGET_MS = 3_000;
111
120
  /** Threshold for a "即将重置" forced fetch: any window resetting within this. */
112
121
  export const RESET_IMMINENT_MS = 60_000;
113
122
 
@@ -121,6 +130,8 @@ export interface StartPollerOptions {
121
130
  minFetchIntervalMs?: number;
122
131
  /** Idle fallback: providers whose last successful fetch is older than this get marked dirty. Default 60s. */
123
132
  idleDirtyMs?: number;
133
+ /** Max wall time the cold-start network refresh may block the caller before detaching to background. Default 3s. */
134
+ startupFetchBudgetMs?: number;
124
135
  /** Fired once at the end of every tick (after syncFromDisk + any fetch). */
125
136
  onTick?: () => void;
126
137
  /** Whether the calling session owns a real UI surface. A non-UI session (in-process task subagent whose ctx.ui is a no-op) must NOT steal the tick-render callback from the UI-owning session. */
@@ -1003,6 +1014,35 @@ async function fetchProvider(
1003
1014
  }
1004
1015
  }
1005
1016
 
1017
+ /**
1018
+ * Run the cold-start fetch but release the caller after `budgetMs` at most.
1019
+ *
1020
+ * `session_start` awaits the poller startup; a blackholed network otherwise
1021
+ * pins the extension handler until the host's 30s gate. The in-flight fetch
1022
+ * keeps running detached (direct fetchers carry their own hard timeout, and
1023
+ * fetchProvider swallows per-provider errors), so the result still lands via
1024
+ * the 1s render tick. `budgetMs <= 0` detaches immediately.
1025
+ */
1026
+ async function fetchDueWithStartupBudget(startFetch: () => Promise<void>, budgetMs: number): Promise<void> {
1027
+ const initial = startFetch();
1028
+ if (budgetMs <= 0) {
1029
+ void initial.then(() => {}, () => {});
1030
+ return;
1031
+ }
1032
+ let settled = false;
1033
+ initial.then(() => { settled = true; }, () => { settled = true; });
1034
+ let budgetTimer: NodeJS.Timeout | undefined;
1035
+ const budget = new Promise<void>((resolve) => {
1036
+ budgetTimer = setTimeout(resolve, budgetMs);
1037
+ });
1038
+ try {
1039
+ await Promise.race([initial, budget]);
1040
+ } finally {
1041
+ clearTimeout(budgetTimer);
1042
+ }
1043
+ if (!settled) return; // budget elapsed: keep the fetch running detached
1044
+ }
1045
+
1006
1046
  /**
1007
1047
  * Fetch every dirty provider whose per-provider minimum-interval floor has
1008
1048
  * elapsed (skipping in-flight ones), consuming the dirty marks on initiation.
@@ -1225,7 +1265,10 @@ export async function startSharedPoller(
1225
1265
  if (fresh) return;
1226
1266
 
1227
1267
  evaluateDirty(computeLoggedIn());
1228
- await fetchDue(authStorage, directFetchers, getApiKey);
1268
+ await fetchDueWithStartupBudget(
1269
+ () => fetchDue(authStorage, directFetchers, getApiKey),
1270
+ opts?.startupFetchBudgetMs ?? DEFAULT_STARTUP_FETCH_BUDGET_MS,
1271
+ );
1229
1272
  })();
1230
1273
  try {
1231
1274
  await startInFlight;
@@ -48,6 +48,12 @@ export interface Painter {
48
48
  header(label: string, brandAnsi: string): string;
49
49
  /** Numeric balance text (e.g. "余额 ¥18.06"), text-colored. */
50
50
  balance(text: string): string;
51
+ /**
52
+ * Muted/secondary spans — dim-colored so they never compete with the
53
+ * content for visual weight: the inline concurrency token, and the bar's
54
+ * closing rule.
55
+ */
56
+ muted(text: string): string;
51
57
  /**
52
58
  * Waveform layer paint — btop row gradient over the provider's brand hue.
53
59
  * `brandAnsi` is the raw brand SGR ('' → accent fallback, like header).
@@ -98,6 +104,7 @@ export const ansiPainter: Painter = {
98
104
  sep: ` ${D}│${R} `,
99
105
  header: (label, brandAnsi) => c(brandAnsi ? BOLD + brandAnsi : BOLD, label),
100
106
  balance: (text) => c(WHITE, text),
107
+ muted: (text) => c(D, text),
101
108
  wave: (brandAnsi, layer, text) => c(layer === 'bottom' ? bottomShade(brandAnsi || CYAN) : brandAnsi || CYAN, text),
102
109
  };
103
110
 
@@ -109,6 +116,7 @@ export function themePainter(theme: PainterTheme): Painter {
109
116
  sep: ` ${theme.fg('dim', '│')} `,
110
117
  header: (label, brandAnsi) => (brandAnsi ? c(brandAnsi, label) : theme.fg('accent', label)),
111
118
  balance: (text) => theme.fg('text', text),
119
+ muted: (text) => theme.fg('dim', text),
112
120
  wave: (brandAnsi, layer, text) =>
113
121
  brandAnsi
114
122
  ? c(layer === 'bottom' ? bottomShade(brandAnsi) : brandAnsi, text)
@@ -123,7 +131,7 @@ export function themePainter(theme: PainterTheme): Painter {
123
131
  // Unknown providers fall back to the omp registry display name, then the raw id.
124
132
  const PROVIDER: Record<string, { label: string; color: string }> = {
125
133
  'zhipu-coding-plan': { label: '智谱', color: PURPLE },
126
- zai: { label: '智谱', color: PURPLE },
134
+ zai: { label: 'Z.AI', color: PURPLE },
127
135
  'kimi-code': { label: 'Kimi', color: CYAN },
128
136
  'minimax-code-cn': { label: 'MiniMax', color: X },
129
137
  'ark-coding-plan': { label: '火山方舟', color: CYAN },
@@ -385,8 +393,28 @@ const padRight = (s: string, width: number): string => {
385
393
  return v >= width ? s : s + ' '.repeat(width - v);
386
394
  };
387
395
 
396
+ /**
397
+ * Per-provider adaptive-concurrency view the inline header token renders:
398
+ * the controller's current limit `L` and the allowed `ceiling`. The assembly
399
+ * layer builds this from `ConcurrencyController.stateFor(provider)`; a provider
400
+ * absent from the map renders exactly as it did before this token existed.
401
+ */
402
+ export interface ConcurrencyInlineView {
403
+ /** Current adaptive limit (L). */
404
+ limit: number;
405
+ /** Upper bound of the allowed range. */
406
+ ceiling: number;
407
+ /** Lease held read-only (no writes this process) → append the `⚠` mark. */
408
+ degraded?: boolean;
409
+ }
410
+
388
411
  export function renderUsageReports(
389
412
  reports: UsageReport[],
413
+ /**
414
+ * Render width in cells. Provider rows IGNORE it (they keep their natural
415
+ * width — roomy over compact), but the closing rule is bounded by it, so it
416
+ * tracks the terminal instead of wrapping.
417
+ */
390
418
  maxWidth = 120,
391
419
  painter: Painter = ansiPainter,
392
420
  placeholderIds?: readonly string[],
@@ -394,6 +422,12 @@ export function renderUsageReports(
394
422
  consumptionTracks?: Map<string, ConsumptionTrack>,
395
423
  usageEstimates?: Map<string, string>,
396
424
  exhaustedProviders?: ReadonlySet<string>,
425
+ /**
426
+ * Inline adaptive-concurrency token per managed provider, appended to that
427
+ * provider's header row (dim, after the name) — never a trailing block.
428
+ * Omitted/empty keeps the pre-existing output byte-for-byte identical.
429
+ */
430
+ concurrencyViews?: ReadonlyMap<string, ConcurrencyInlineView>,
397
431
  ): string[] {
398
432
  // Placeholder providers are logged-in but unfetched this round. Their ids are
399
433
  // explicit (never inferred from `limits.length === 0`); dedupe against reports
@@ -403,6 +437,17 @@ export function renderUsageReports(
403
437
  if (!reports?.length && placeholders.length === 0) return [];
404
438
  const loading = loadingProviders ?? new Set<string>();
405
439
 
440
+ // Inline concurrency token: header row only, and only for managed providers.
441
+ // Runs BEFORE the per-block width pass, so the injected token participates
442
+ // in the block's natural width and multi-block separators stay aligned.
443
+ const withConcurrency = (provider: string, col: string[]): string[] => {
444
+ const view = concurrencyViews?.get(provider);
445
+ if (!view || col.length === 0) return col;
446
+ // Token shape: `L/ceiling`, plus `⚠` on a read-only-lease degradation.
447
+ const token = `${view.limit}/${view.ceiling}${view.degraded ? '⚠' : ''}`;
448
+ return [`${col[0]} ${painter.muted(token)}`, ...col.slice(1)];
449
+ };
450
+
406
451
  // Build each provider column (header + usage / placeholder). Each block
407
452
  // aligns its OWN lines to its own natural width (header/body/waveform
408
453
  // share that block's width so the separators stack vertically) — blocks
@@ -422,12 +467,12 @@ export function renderUsageReports(
422
467
  // Second-level estimate override replaces the usage line (balance style).
423
468
  const est = usageEstimates?.get(r.provider);
424
469
  if (est && col.length >= 2) col[1] = painter.balance(est);
425
- return { provider: r.provider, col, width: 0 };
470
+ return { provider: r.provider, col: withConcurrency(r.provider, col), width: 0 };
426
471
  });
427
472
  for (const id of placeholders) {
428
473
  cols.push({
429
474
  provider: id,
430
- col: buildPlaceholderColumn(id, painter, loading.has(id), exhaustedProviders?.has(id)),
475
+ col: withConcurrency(id, buildPlaceholderColumn(id, painter, loading.has(id), exhaustedProviders?.has(id))),
431
476
  width: 0,
432
477
  });
433
478
  }
@@ -459,7 +504,8 @@ export function renderUsageReports(
459
504
  // beyond `maxWidth` rather than squeezing content or breaking to a second
460
505
  // line (user preference: roomy over compact). Blocks shorter than the
461
506
  // tallest contribute a cell padded to that block's own width on missing
462
- // rows, so every separator keeps its column.
507
+ // rows, so every separator keeps its column. Only the rule appended below
508
+ // is bounded by `maxWidth`.
463
509
  const multi = valid.length > 1;
464
510
  const n = Math.max(...valid.map((c) => c.col.length));
465
511
  const out: string[] = [];
@@ -470,6 +516,13 @@ export function renderUsageReports(
470
516
  });
471
517
  out.push(cells.join(painter.sep));
472
518
  }
519
+ // Closing rule under the whole bar — the layout's ONLY width-bounded row
520
+ // (content rows keep their natural width above). `maxWidth` is the caller's
521
+ // live render width: the terminal width on the component path (re-read on
522
+ // every resize), columns minus slack on the print path. Bounding it here is
523
+ // what keeps it from wrapping — an over-wide row soft-wraps the terminal,
524
+ // and this bar never wraps.
525
+ out.push(painter.muted('─'.repeat(Math.max(1, Math.trunc(maxWidth)))));
473
526
  return out;
474
527
  }
475
528
  // ── token consumption waveform (Braille, design Decision 5) ─────────
@@ -1,7 +1,7 @@
1
1
  import { truncateToWidth, type Component } from "@oh-my-pi/pi-tui";
2
2
  import type { ExtensionUiComponentFactory } from "@oh-my-pi/pi-coding-agent";
3
3
  import type { UsageReport } from "@oh-my-pi/pi-ai";
4
- import { buildColumn, renderUsageReports, themePainter, type Painter, type ConsumptionTrack } from "./usage-render.js";
4
+ import { buildColumn, renderUsageReports, themePainter, type Painter, type ConsumptionTrack, type ConcurrencyInlineView } from "./usage-render.js";
5
5
 
6
6
  /**
7
7
  * A single provider rendered as a standalone, reusable pi-tui `Component`.
@@ -58,6 +58,7 @@ export class UsageTable implements Component {
58
58
  readonly #consumptionTracks?: Map<string, ConsumptionTrack>;
59
59
  readonly #usageEstimates?: Map<string, string>;
60
60
  readonly #exhaustedProviders?: ReadonlySet<string>;
61
+ readonly #concurrencyViews?: ReadonlyMap<string, ConcurrencyInlineView>;
61
62
 
62
63
  constructor(
63
64
  reports: UsageReport[],
@@ -67,6 +68,12 @@ export class UsageTable implements Component {
67
68
  consumptionTracks?: Map<string, ConsumptionTrack>,
68
69
  usageEstimates?: Map<string, string>,
69
70
  exhaustedProviders?: ReadonlySet<string>,
71
+ /**
72
+ * Inline per-provider concurrency tokens (see `renderUsageReports`),
73
+ * appended to each managed provider's header row. Omitted/empty →
74
+ * byte-identical legacy output.
75
+ */
76
+ concurrencyViews?: ReadonlyMap<string, ConcurrencyInlineView>,
70
77
  ) {
71
78
  this.#reports = reports;
72
79
  this.#painter = painter;
@@ -75,11 +82,14 @@ export class UsageTable implements Component {
75
82
  this.#consumptionTracks = consumptionTracks;
76
83
  this.#usageEstimates = usageEstimates;
77
84
  this.#exhaustedProviders = exhaustedProviders;
85
+ this.#concurrencyViews = concurrencyViews;
78
86
  }
79
87
 
80
88
  render(width: number): readonly string[] {
81
89
  // Per-provider consumption tracks are threaded into the layout kernel,
82
- // which embeds each chart inside its own provider column.
90
+ // which embeds each chart inside its own provider column. The live
91
+ // `width` also sizes the kernel's closing rule, so a resize re-render
92
+ // re-fits the rule to the terminal.
83
93
  return renderUsageReports(
84
94
  this.#reports,
85
95
  Math.max(1, Math.trunc(width)),
@@ -89,6 +99,7 @@ export class UsageTable implements Component {
89
99
  this.#consumptionTracks,
90
100
  this.#usageEstimates,
91
101
  this.#exhaustedProviders,
102
+ this.#concurrencyViews,
92
103
  );
93
104
  }
94
105
 
@@ -103,7 +114,8 @@ export class UsageTable implements Component {
103
114
  * current reports (mirroring the dashboard precedent), which re-injects the then-
104
115
  * current theme. Returns a `Container`-free `UsageTable` directly: rows extend
105
116
  * at their natural width (the kernel never wraps — a row can run past `width`),
106
- * and `ProviderCard.render`'s `truncateToWidth` is the only truncation seam.
117
+ * the kernel's closing rule is the one row that does track `width`, and
118
+ * `ProviderCard.render`'s `truncateToWidth` is the only truncation seam.
107
119
  * Wrapping each row in `pi-tui`'s `Text` would both drop the single-row
108
120
  * natural-width blank separators (Text renders empty input as `[]`) and repad
109
121
  * every line to the full width — losing byte parity with the RPC/print path
@@ -116,6 +128,8 @@ export function createUsageWidget(
116
128
  consumptionTracks?: Map<string, ConsumptionTrack>,
117
129
  usageEstimates?: Map<string, string>,
118
130
  exhaustedProviders?: ReadonlySet<string>,
131
+ /** Inline per-provider concurrency tokens appended to header rows (optional). */
132
+ concurrencyViews?: ReadonlyMap<string, ConcurrencyInlineView>,
119
133
  ): ExtensionUiComponentFactory {
120
- return (_tui, theme) => new UsageTable(reports, themePainter(theme), placeholderIds, loadingProviders, consumptionTracks, usageEstimates, exhaustedProviders);
134
+ return (_tui, theme) => new UsageTable(reports, themePainter(theme), placeholderIds, loadingProviders, consumptionTracks, usageEstimates, exhaustedProviders, concurrencyViews);
121
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genee/omp-opsx-addon",
3
- "version": "0.7.0",
3
+ "version": "0.9.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",
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: opsx-orchestration-protocol
3
+ description: opsx 四角色(coder / code-reviewer / planner / proposal-reviewer)共享的自定义编排协议——scratchpad 四区共享缓存与角色读写矩阵、supersede 权威语义与两档客观判据、无关状态硬栅栏(仅 coder)、P0/P1 评审闭环与轮次上限、报告契约、生效与兜底。
4
+ ---
5
+
6
+ # OpenSpec 四角色编排协议
7
+
8
+ 本技能是 `@genee/omp-opsx-addon` 的 coder / code-reviewer / planner / proposal-reviewer 四个 agent 共享编排协议的**单一真源**。四份 agent 模板不复述本协议,只保留指向本技能的指针;协议口径的维护只改本文件。
9
+
10
+ ## scratchpad 四区共享缓存与角色读写矩阵
11
+
12
+ 变更目录下维护 `openspec/changes/<name>/scratchpad.md` 作为四角色共享探索缓存(与 proposal.md / design.md / tasks.md 同级),固定四阶段分区:
13
+
14
+ - `## 调研与设计`:调研结论、涉及文件(`path:line`)、已排除方案
15
+ - `## 提案审查关注点`:proposal-reviewer 审查报告中的 P0/P1 与关注点(经主 agent 中转回流)
16
+ - `## 实现探索`:coder 每轮 append 的关键符号/数据流、新增涉及文件、验证/构建命令、已排除假设
17
+ - `## 代码审查范围`:code-reviewer 审查报告中的 P0/P1 与验证范围结论(经主 agent 中转回流)
18
+
19
+ 条目一律标注轮次与角色,形如 `- [R<n> planner] <结论>`、`- [R<n> coder] <结论>`、`- [R<n> proposal-reviewer] <结论>`、`- [R<n> code-reviewer] <结论>`;涉及文件记 `path:line`。
20
+
21
+ 读写矩阵:
22
+
23
+ | 角色 | scratchpad.md 权限 |
24
+ | --- | --- |
25
+ | planner | 创建 + 写入:出提案时 MUST 创建并写入四阶段骨架,预填「调研与设计」初始骨架(调研结论、涉及文件、已排除方案) |
26
+ | coder | 读取 + 写入:开工前 MUST 先读;交付前把本轮新探索结论 append 进「实现探索」区;收到主 agent 中转的 code-reviewer P0/P1 时,开工前先把关注点 append 进「代码审查范围」区再修复 |
27
+ | proposal-reviewer | 只读,不改写:审查前 MUST 先读(若存在),复用其中调研结论与涉及文件,避免重复 read/grep;工具集不含 write/edit,物理上无法写入 |
28
+ | code-reviewer | 只读,不改写:审查前 MUST 先读(若存在),复用其中调研结论与涉及文件,避免重复 read/grep;工具集不含 write/edit,物理上无法写入 |
29
+
30
+ 规则:
31
+
32
+ - **复用与增量**:开工前复用已记录结论,禁止重复探索;后续轮次只做增量——仅探索未记录的文件/符号/假设,不重复 read/grep 已记录内容。
33
+ - **append-only**:不得改写或删除他人结论。
34
+ - **创建门槛**:提案涉及跨文件或多轮协作时 MUST 创建 scratchpad.md 并写入四阶段骨架;角色专属豁免(不适用本技能编排的场景)只写在各角色模板的「工作边界」节,本技能不复述。
35
+ - **reviewer 关注点回流**:审查员发现的 P0/P1 与关注点写入审查报告,由主 agent 中转回流给可写角色(planner / coder),由其在对应分区 append;审查员不直接写 scratchpad.md。
36
+ - **supersede 权威语义**:识别 `- [R<n> coder] supersede:` 条目,以紧随其后的新结论为权威,旧结论(含旧 `path:line`)不再作为调研或验证依据。
37
+ - **结论与现状不符**:审查员发现 scratchpad.md 结论与代码现状不符时,作为 P0/P1 审查发现发回,不改写。
38
+
39
+ ## 客观判据与两档处置
40
+
41
+ 执行中发现代码现实与 scratchpad.md 记录不一致时,按两档客观判据处置;判据仅为「偏差是否影响任何 task 的前提或产出定义」,禁止主观估量「问题大小」:
42
+
43
+ - **档一:事实快照过期**——偏差不影响任何 task 的前提或产出定义(纯探索性信息:路径 / 行号 / 符号 / 命令漂移)。处置:继续执行当前任务,不终止;在「实现探索」区 append supersede 修正。
44
+ - **档二:契约动摇**——偏差导致 tasks / proposal / design / specs 的有效性存疑(前提不成立、产出定义变、代码现状与设计决策冲突)。处置:立即停止实现,输出 STATUS: blocked(无 SESSION)并说明冲突点,交由主 agent 裁决(改提案 / 确认「现状即新设计」/ 开新 change)。
45
+ - supersede 标注格式:`- [R<n> coder] supersede: <旧结论摘要>`,随后一行以 `- [R<n> coder]` 标注新结论;两条均落「实现探索」分区,旧结论保留(append-only),不删除。
46
+ - 交付摘要 SUMMARY 义务:本轮发生过 supersede 时,输出格式中的 SUMMARY 行 MUST 提及本次 supersede 清单。
47
+
48
+ ## 与本次任务无关的异常状态(硬栅栏)【无关状态栅栏】
49
+ 本节仅适用于 coder(实现类);reviewer / planner / proposal-reviewer MUST NOT 受本节限制(其职责要求穷尽调查与考古)。
50
+
51
+ 执行中发现异常状态时,按既有客观判据判档——偏差不影响任何 task 的前提或产出定义(判据为否)即属**无关状态**,禁止主观估量「问题大小」。典型无关形态:别人未提交的改动、无关失败测试(无关红测)、无关脏文件、工作树状态与预期不符。
52
+
53
+ - **前置豁免(最小相关性核查不受限)**:判定「偏差是否影响任何 task 的前提或产出定义」所需的核查(如运行 `git status`、确认被改动文件是否落在本次改动面内)不受本栅栏限制;本栅栏禁止的是**判定为无关之后**的因果考古与因此暂停 task。
54
+ - **硬栅栏**:对判定为无关的状态,MUST NOT 调查——MUST NOT 以 `git log`/`git show`/`git diff`/`git blame` 等手段追查改动来源,MUST NOT 追查「谁改的 / 为何改」,MUST NOT 因该状态暂停当前 task。
55
+ - **处置(显式留痕,不许静默)**:记录为假设 → 写入交付摘要 `SUMMARY` 行 → 继续执行当前任务;缺席的正当行为是问题被显式记录、可审计,而非问题消失。
56
+ - **不吞真阻塞**:本栅栏仅约束无关状态;缺依赖、契约动摇(tasks / proposal / design / specs 有效性存疑,即档二)、权限不足、需用户决策等阻塞本次任务的情形,仍 MUST 立即输出 STATUS: blocked 并附证据,两条判据共存、不互斥。
57
+ - **与既有验证门槛规则一致**:项目级全量校验失败可归因并发 sibling 时,完整动作序列 = 最小相关性核查(含排除自身改动)→ 判定无关 → 附「已排查与本改动无关」证据写入 `SUMMARY` → 继续,不调查改动来源;既有规则的义务不变。
58
+
59
+ ## 评审闭环与轮次上限
60
+
61
+ 严重度:**P0/P1** = 必须修复,阻塞通过;**P2+** = 可选修复,不阻塞通过。
62
+
63
+ - **Loop 1(Propose → Review)**:planner 完成提案 → proposal-reviewer 审查 → P0/P1 回传主 agent → 委派 planner 修复;往复最多 2 轮。
64
+ - **Loop 2(Code → Review)**:coder 完成(交付前自验证)→ code-reviewer 审阅 + 全局验证 → P0/P1 回传主 agent → 委派 coder 修复;往复上限「最多 3 轮(含首次实现与修复)」,仍无法解决则 STATUS: blocked。
65
+ - **审查员只报告、不改代码**:结论为 APPROVE / REQUEST_CHANGES / BLOCKED;P0/P1 经主 agent 转发给下一轮 coder / planner,审查员不直接改代码、也不改 scratchpad.md。
66
+ - **coder 侧协作流程(三条义务,逐字保持、不弱化)**:
67
+ - **① 完成实现后**:向主 agent 报告,由主 agent 决定是否委派审查
68
+ - **② 收到审查反馈(P0/P1)**:反馈统一由 code-reviewer 回传(含全局验证失败);开工时先把关注点 append 进「代码审查范围」区,用 read/edit/bash 修复后重新标记 ACTION: REVIEW_REQUIRED
69
+ - **③ 轮次上限**:最多 3 轮(含首次实现与修复);仍无法解决则 STATUS: blocked
70
+
71
+ ## 报告契约
72
+
73
+ - **coder**:以固定报告块收尾——`ACTION`(REVIEW_REQUIRED)/ `CHANGE` / `STATUS`(success | partial | blocked)/ `SESSION`(仅 STATUS: blocked 且需 resume 时填写)/ `SUMMARY` / `TASKS` / `FILES`;本轮有 supersede 时 `SUMMARY` 行 MUST 提及 supersede 清单。修复 P0/P1 后输出中增加「本轮修复」章节;需再审时 `ACTION` 仍为 `REVIEW_REQUIRED`。
74
+ - **reviewer(code-reviewer / proposal-reviewer)**:以固定报告块收尾——`结论`(APPROVE / REQUEST_CHANGES / BLOCKED)/ `轮次` / `关键问题`(P0/P1)/ `改进建议`(P2+);code-reviewer 另附全局验证节。
75
+ - **上报去向**:APPROVE → 报告通过;REQUEST_CHANGES + P0/P1 → 由主 agent 委派对应角色修复;REQUEST_CHANGES + 仅 P2+ → 视为通过,建议可选;BLOCKED → 说明原因,由用户决策。
76
+ - 各角色一律使用各自模板的「输出格式」块收尾;子 agent 无用户,不得采用面向主会话的交互式提问或等待措辞(边界见下节)。
77
+
78
+ ## 生效与兜底
79
+
80
+ - **注入方式**:本技能经各角色 agent frontmatter 的 `autoloadSkills` 注入——协议技能(本技能)对四个角色**恒在**;coder / planner 各两个技能(本技能 + 一个 openspec 官方技能),两个 reviewer 各一个(仅本技能)。逐角色的精确取值以各自 agent 模板 frontmatter 为单一真源,本节不复述。
81
+ - **兜底指针**:`autoloadSkills` 中的**每个技能各一行** `skill://<name>` 指针(逐行同构、不合并);技能未自动加载时,用 Skill tool 读取该指针并遵循其流程(协议技能即 `skill://opsx-orchestration-protocol`)。逐角色的精确指针取值以各自模板的「技能」节为单一真源,本节不复述。
82
+ - **技能不可读 → 前提缺失**:角色 `autoloadSkills` 中的技能名不可解析、且用 Skill tool 读取 `skill://<name>` 亦不可读(技能不在场)时,视为前提缺失——输出 `STATUS: blocked`(无 SESSION)并报缺失技能名交由主 agent 决策;MUST NOT 凭记忆复述该技能的流程代替之。
83
+ - **官方技能正文的子 agent 边界**:`openspec-apply-change` / `openspec-propose` 正文面向主会话撰写,对子 agent 的适用面:
84
+ - **适用**——流程与产物定义:选变更、读 tasks/proposal/design/specs、逐 task 实现、`- [ ]` → `- [x]`、阻塞时不猜测。
85
+ - **不适用**——子 agent 无用户:`announce` 进度播报(改由本角色模板的「输出格式」块收尾)、「ask the user / 询问用户」、「pause / 等待用户输入」、progress / pause 报告块一律不采用。
86
+ - 需用户决策的情形仍按 `STATUS: blocked` 上报主 agent,由其决策;报告一律使用各自模板的「输出格式」块。
87
+ - **官方技能文件保持上游原样**:`.omp/skills/openspec-apply-change` / `openspec-propose` MUST NOT 被本插件修改;子 agent 边界只在本节单点声明。