pi-orchestrator-config 4.2.1 → 4.3.2

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 (92) hide show
  1. package/README.md +52 -61
  2. package/agents/git-expert.md +5 -1
  3. package/agents/github-expert.md +4 -1
  4. package/extensions/acpx-provider/runtime-models.ts +18 -6
  5. package/extensions/cli-provider/agents/claude.ts +3 -2
  6. package/extensions/cli-provider/agents/cursor.ts +3 -5
  7. package/extensions/cli-provider/agents/gemini.ts +3 -2
  8. package/extensions/cli-provider/discover.ts +7 -7
  9. package/extensions/cli-provider/index.ts +5 -0
  10. package/extensions/cli-provider/runtime-models.ts +18 -6
  11. package/extensions/cli-provider/session-reaper.ts +5 -7
  12. package/extensions/cli-provider/sessions.ts +4 -0
  13. package/extensions/cli-provider/shared/discover-cache.ts +17 -176
  14. package/extensions/coms/coms-p2p.ts +1719 -423
  15. package/extensions/coms/coms-shared.ts +261 -39
  16. package/extensions/coms/coms-wrapper.ts +100 -62
  17. package/extensions/coms/index.ts +8 -4
  18. package/extensions/coms/mixed-turn.ts +26 -0
  19. package/extensions/coms/probe-socket.ts +42 -0
  20. package/extensions/coms/themeMap.ts +0 -1
  21. package/extensions/image-gen/image-gen.ts +19 -1
  22. package/extensions/orchestrator/async-agents.ts +509 -85
  23. package/extensions/orchestrator/cron.ts +14 -8
  24. package/extensions/orchestrator/dreaming.ts +31 -24
  25. package/extensions/orchestrator/enforcement-helpers.ts +459 -17
  26. package/extensions/orchestrator/enforcement-rules.ts +5 -4
  27. package/extensions/orchestrator/enforcement.ts +79 -25
  28. package/extensions/orchestrator/extended-autocomplete.ts +7 -0
  29. package/extensions/orchestrator/git-helpers.ts +2 -2
  30. package/extensions/orchestrator/index.ts +23 -10
  31. package/extensions/orchestrator/markdown-transformer.ts +313 -0
  32. package/extensions/orchestrator/memory-embeddings.ts +36 -34
  33. package/extensions/orchestrator/memory-provenance.ts +29 -25
  34. package/extensions/orchestrator/memory-scoring.ts +23 -18
  35. package/extensions/orchestrator/memory-tools.ts +10 -3
  36. package/extensions/orchestrator/overlay-dashboard.ts +17 -5
  37. package/extensions/orchestrator/pi-config-review-state.ts +44 -33
  38. package/extensions/orchestrator/project-settings.ts +220 -21
  39. package/extensions/orchestrator/review-porcelain.ts +4 -0
  40. package/extensions/orchestrator/review-ui.ts +11 -5
  41. package/extensions/orchestrator/rule-placeholders.ts +384 -1
  42. package/extensions/orchestrator/rules.ts +80 -132
  43. package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
  44. package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
  45. package/extensions/orchestrator/settings-tui.ts +636 -0
  46. package/extensions/orchestrator/state-jsonl.ts +431 -0
  47. package/extensions/orchestrator/subagent-tool.ts +105 -50
  48. package/extensions/orchestrator/sync-limit.ts +23 -0
  49. package/extensions/orchestrator/task-lifecycle.ts +77 -67
  50. package/extensions/orchestrator/utils.ts +8 -5
  51. package/extensions/pidash/pidash.ts +193 -156
  52. package/extensions/pidiff/pidiff.ts +64 -38
  53. package/extensions/pidiff/spawn-lock.ts +62 -0
  54. package/extensions/pitasks/index.ts +430 -0
  55. package/extensions/pitasks/reminders.ts +37 -0
  56. package/extensions/pitasks/task-store.ts +467 -0
  57. package/extensions/pitasks/task-tools.ts +375 -0
  58. package/extensions/pitasks/task-widget.ts +181 -0
  59. package/extensions/pitasks/types.ts +38 -0
  60. package/extensions/providers/acpx-driver.ts +12 -2
  61. package/extensions/providers/built-in-drivers.ts +6 -6
  62. package/extensions/providers/claude-driver.ts +1 -1
  63. package/extensions/providers/cursor-cli-driver.ts +1 -1
  64. package/extensions/providers/gemini-driver.ts +1 -1
  65. package/extensions/providers/index.ts +144 -26
  66. package/extensions/providers/initialized-guard.ts +27 -0
  67. package/extensions/providers/restore-default-model.ts +596 -0
  68. package/extensions/providers/session-shutdown.ts +39 -0
  69. package/extensions/shared/coms-active.ts +23 -0
  70. package/extensions/shared/create-runtime-provider.ts +2 -0
  71. package/extensions/shared/daemon-manager.ts +0 -8
  72. package/extensions/shared/file-logger.ts +132 -25
  73. package/extensions/shared/logger.ts +50 -0
  74. package/extensions/shared/models-dev.ts +421 -0
  75. package/extensions/shared/oneshot.ts +116 -0
  76. package/extensions/shared/resolve-binary.ts +191 -0
  77. package/extensions/shared/types.ts +3 -0
  78. package/package.json +7 -2
  79. package/prompts/coderabbit-rate-limit.md +2 -0
  80. package/rules/00-orchestrator-core.md +3 -0
  81. package/rules/10-agent-routing.md +1 -1
  82. package/rules/20-code-review-loop.md +38 -16
  83. package/rules/30-prompt-templates.md +4 -0
  84. package/rules/35-memory.md +3 -3
  85. package/rules/40-critical-rules.md +3 -0
  86. package/rules/55-coms-protocol.md +84 -18
  87. package/rules/60-task-tracking.md +2 -0
  88. package/settings-keys.json +481 -20
  89. package/templates/coms-feature-manager-prompt.md +4 -6
  90. package/extensions/coms/coms-net-server.ts +0 -1642
  91. package/extensions/coms/coms-net-wrapper.ts +0 -521
  92. package/extensions/coms/coms-net.ts +0 -1589
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Pure placeholder substitution for rules/*.md and agent system-prompt text.
2
+ * Pure placeholder substitution and conditional assembly for rules/*.md
3
+ * and agent system-prompt text.
3
4
  *
4
5
  * Kept in its own module (no heavy imports) so it can be unit tested without
5
6
  * pulling in the full orchestrator dependency chain (e.g. rules.ts → async-agents.ts
@@ -7,6 +8,10 @@
7
8
  * the test install). Callers pass a resolve function for settings lookups.
8
9
  */
9
10
 
11
+ import { createLogger } from "../shared/logger.js";
12
+
13
+ const log = createLogger("rules");
14
+
10
15
  export function substituteRulePlaceholders(text: string, values: { reviewLoopMaxCycles: number }): string {
11
16
  return text.replaceAll("{{REVIEW_LOOP_MAX_CYCLES}}", String(values.reviewLoopMaxCycles));
12
17
  }
@@ -36,3 +41,381 @@ export function substituteSettingsPlaceholders(
36
41
  return JSON.stringify(result);
37
42
  });
38
43
  }
44
+
45
+ /** Truthiness for settings-gated rule blocks. */
46
+ export function isSettingTruthy(value: unknown): boolean {
47
+ if (value === false || value === null || value === undefined || value === "" || value === 0) {
48
+ log.debug("isSettingTruthy", { result: false, kind: value === "" ? "empty-string" : String(value) });
49
+ return false;
50
+ }
51
+ if (Array.isArray(value) && value.length === 0) {
52
+ log.debug("isSettingTruthy", { result: false, kind: "empty-array" });
53
+ return false;
54
+ }
55
+ if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value as object).length === 0) {
56
+ log.debug("isSettingTruthy", { result: false, kind: "empty-object" });
57
+ return false;
58
+ }
59
+ log.debug("isSettingTruthy", { result: true, kind: Array.isArray(value) ? "array" : typeof value });
60
+ return true;
61
+ }
62
+
63
+ /** Parse comparison / truthy condition text after `IF:` or `IFNOT:`. */
64
+ export function parseConditionExpr(expr: string): {
65
+ key: string;
66
+ op: "truthy" | "eq" | "neq";
67
+ literal?: unknown;
68
+ } | null {
69
+ const trimmed = expr.trim();
70
+ if (!trimmed) {
71
+ log.debug("parseConditionExpr empty");
72
+ return null;
73
+ }
74
+ const cmp = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*(==|!=)\s*(.*)$/);
75
+ if (cmp) {
76
+ const parsed = {
77
+ key: cmp[1],
78
+ op: (cmp[2] === "==" ? "eq" : "neq") as "eq" | "neq",
79
+ literal: parseConditionLiteral(cmp[3].trim()),
80
+ };
81
+ log.debug("parseConditionExpr", parsed);
82
+ return parsed;
83
+ }
84
+ const keyOnly = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/);
85
+ if (keyOnly) {
86
+ const parsed = { key: keyOnly[1], op: "truthy" as const };
87
+ log.debug("parseConditionExpr", parsed);
88
+ return parsed;
89
+ }
90
+ log.debug("parseConditionExpr malformed", trimmed);
91
+ return null;
92
+ }
93
+
94
+ /** Parse `true`/`false`/`null`/number/string literals used in `key==value` conditions. */
95
+ export function parseConditionLiteral(raw: string): unknown {
96
+ let result: unknown = raw;
97
+ if (raw === "true") result = true;
98
+ else if (raw === "false") result = false;
99
+ else if (raw === "null") result = null;
100
+ else if (
101
+ (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
102
+ (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2)
103
+ ) {
104
+ result = raw.slice(1, -1);
105
+ } else if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(raw)) {
106
+ result = Number(raw);
107
+ }
108
+ log.debug("parseConditionLiteral", { raw, resultType: typeof result });
109
+ return result;
110
+ }
111
+
112
+ export type ConditionalEvalOpts = {
113
+ knownKeys?: string[];
114
+ /** Feature predicates consulted for truthy `{{IF:key}}` when key is not a setting. */
115
+ featurePredicates?: Record<string, () => boolean>;
116
+ onWarn?: (msg: string) => void;
117
+ };
118
+
119
+ type OpenKind = "if" | "ifnot";
120
+
121
+ type OpenMarker = {
122
+ kind: OpenKind;
123
+ cond: string;
124
+ index: number;
125
+ end: number;
126
+ };
127
+
128
+ const MARKER_RE = /\{\{(IFNOT:([^}]+)|IF:([^}]+)|\/IFNOT|\/IF)\}\}/g;
129
+
130
+ function markerRe(): RegExp {
131
+ return new RegExp(MARKER_RE.source, "g");
132
+ }
133
+
134
+ function findNextOpen(text: string, from: number): OpenMarker | null {
135
+ const re = markerRe();
136
+ re.lastIndex = from;
137
+ let m: RegExpExecArray | null;
138
+ while ((m = re.exec(text)) !== null) {
139
+ const full = m[0];
140
+ if (full.startsWith("{{IFNOT:")) {
141
+ return { kind: "ifnot", cond: m[2], index: m.index, end: m.index + full.length };
142
+ }
143
+ if (full.startsWith("{{IF:")) {
144
+ return { kind: "if", cond: m[3], index: m.index, end: m.index + full.length };
145
+ }
146
+ // stray close — leave for later scan
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Find the matching closer for `open`, tracking a stack of open kinds.
153
+ * A mismatched closer at any depth does not pop — unbalanced → null.
154
+ */
155
+ function findMatchingClose(
156
+ text: string,
157
+ open: OpenMarker,
158
+ ): { index: number; end: number } | null {
159
+ const stack: OpenKind[] = [open.kind];
160
+ const re = markerRe();
161
+ re.lastIndex = open.end;
162
+ let m: RegExpExecArray | null;
163
+ while ((m = re.exec(text)) !== null) {
164
+ const full = m[0];
165
+ if (full.startsWith("{{IF:")) {
166
+ stack.push("if");
167
+ continue;
168
+ }
169
+ if (full.startsWith("{{IFNOT:")) {
170
+ stack.push("ifnot");
171
+ continue;
172
+ }
173
+ if (full === "{{/IF}}" || full === "{{/IFNOT}}") {
174
+ const expectedKind: OpenKind = full === "{{/IF}}" ? "if" : "ifnot";
175
+ const top = stack[stack.length - 1];
176
+ if (top !== expectedKind) {
177
+ // Mismatched closer — do not pop; treat as unbalanced for this open.
178
+ return null;
179
+ }
180
+ stack.pop();
181
+ if (stack.length === 0) {
182
+ return { index: m.index, end: m.index + full.length };
183
+ }
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+
189
+ function conditionHolds(
190
+ condRaw: string,
191
+ invert: boolean,
192
+ resolve: (key: string) => unknown,
193
+ opts?: ConditionalEvalOpts,
194
+ ): boolean {
195
+ const parsed = parseConditionExpr(condRaw);
196
+ if (!parsed) {
197
+ opts?.onWarn?.(`malformed conditional condition: ${condRaw}`);
198
+ return false;
199
+ }
200
+ const { key, op, literal } = parsed;
201
+
202
+ // Truthy op: feature predicates win when the key is registered as a feature
203
+ // (does not require knownKeys — features are not settings keys).
204
+ if (op === "truthy" && opts?.featurePredicates && key in opts.featurePredicates) {
205
+ let holds = false;
206
+ try {
207
+ holds = opts.featurePredicates[key]() === true;
208
+ } catch (e: any) {
209
+ opts.onWarn?.(`feature predicate ${key} threw: ${e?.message ?? String(e)}`);
210
+ holds = false;
211
+ }
212
+ return invert ? !holds : holds;
213
+ }
214
+
215
+ let value: unknown = undefined;
216
+ if (opts?.knownKeys && !opts.knownKeys.includes(key)) {
217
+ opts.onWarn?.(`unknown setting key in conditional: ${key}`);
218
+ // Fail closed: unknown keys never keep the block, including {{IFNOT:typo}}.
219
+ log.warn("conditionHolds fail-closed unknown key", { key, invert, op });
220
+ return false;
221
+ } else {
222
+ value = resolve(key);
223
+ }
224
+ let holds: boolean;
225
+ if (op === "truthy") {
226
+ holds = isSettingTruthy(value);
227
+ } else if (op === "eq") {
228
+ holds = value === literal;
229
+ } else {
230
+ holds = value !== literal;
231
+ }
232
+ return invert ? !holds : holds;
233
+ }
234
+
235
+ /**
236
+ * Collapse blank lines between consecutive markdown table rows so
237
+ * removed/kept IF markers do not break table adjacency (`|...|` rows).
238
+ */
239
+ export function collapseBlankLinesBetweenTableRows(text: string): string {
240
+ // Two or more newlines between lines that both start with `|` → single newline
241
+ return text.replace(/(\|[^\n]*)\n(?:[ \t]*\n)+(?=\|)/g, "$1\n");
242
+ }
243
+
244
+ /**
245
+ * Evaluate `{{IF:key}}...{{/IF}}` / `{{IFNOT:key}}...{{/IFNOT}}` blocks
246
+ * (including `key==value` / `key!=value`). Nesting supported.
247
+ * Malformed/unbalanced markers: leave text as-is and warn — never throw.
248
+ */
249
+ export function evaluateConditionalBlocks(
250
+ text: string,
251
+ resolve: (key: string) => unknown,
252
+ opts?: ConditionalEvalOpts,
253
+ ): string {
254
+ try {
255
+ const processed = processRegion(text, resolve, opts);
256
+ return collapseBlankLinesBetweenTableRows(processed);
257
+ } catch (e: any) {
258
+ opts?.onWarn?.(`conditional evaluation failed: ${e?.message ?? String(e)}`);
259
+ return text;
260
+ }
261
+ }
262
+
263
+ function processRegion(
264
+ region: string,
265
+ resolve: (key: string) => unknown,
266
+ opts?: ConditionalEvalOpts,
267
+ ): string {
268
+ let result = "";
269
+ let i = 0;
270
+ while (i < region.length) {
271
+ // Warn on stray closes before the next open
272
+ const peekRe = markerRe();
273
+ peekRe.lastIndex = i;
274
+ const peek = peekRe.exec(region);
275
+ if (peek && (peek[0] === "{{/IF}}" || peek[0] === "{{/IFNOT}}")) {
276
+ const open = findNextOpen(region, i);
277
+ if (!open || open.index > peek.index) {
278
+ opts?.onWarn?.(`unbalanced conditional marker: ${peek[0]}`);
279
+ result += region.slice(i, peek.index + peek[0].length);
280
+ i = peek.index + peek[0].length;
281
+ continue;
282
+ }
283
+ }
284
+
285
+ const open = findNextOpen(region, i);
286
+ if (!open) {
287
+ result += region.slice(i);
288
+ break;
289
+ }
290
+ result += region.slice(i, open.index);
291
+ const close = findMatchingClose(region, open);
292
+ if (!close) {
293
+ opts?.onWarn?.(`unbalanced conditional block starting at {{${open.kind === "if" ? "IF" : "IFNOT"}:${open.cond}}}`);
294
+ // Leave from open marker to end as-is (do not throw)
295
+ result += region.slice(open.index);
296
+ break;
297
+ }
298
+ const body = region.slice(open.end, close.index);
299
+ const keep = conditionHolds(open.cond, open.kind === "ifnot", resolve, opts);
300
+ if (keep) {
301
+ result += processRegion(body, resolve, opts);
302
+ }
303
+ i = close.end;
304
+ }
305
+ return result;
306
+ }
307
+
308
+ /**
309
+ * Parse a simple YAML-ish frontmatter block (`---` / `key: value` / `---`).
310
+ * No attrs → body = raw.
311
+ */
312
+ export function parseRuleFrontmatter(raw: string): { attrs: Record<string, string>; body: string } {
313
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
314
+ if (!match) {
315
+ return { attrs: {}, body: raw };
316
+ }
317
+ const attrs: Record<string, string> = {};
318
+ for (const line of match[1].split(/\r?\n/)) {
319
+ const trimmed = line.trim();
320
+ if (!trimmed || trimmed.startsWith("#")) continue;
321
+ const colon = trimmed.indexOf(":");
322
+ if (colon <= 0) continue;
323
+ const key = trimmed.slice(0, colon).trim();
324
+ const value = trimmed.slice(colon + 1).trim();
325
+ if (key) attrs[key] = value;
326
+ }
327
+ return { attrs, body: match[2] };
328
+ }
329
+
330
+ export type FrontmatterGateOpts = {
331
+ resolveSetting: (key: string) => unknown;
332
+ featurePredicates: Record<string, () => boolean>;
333
+ knownKeys?: string[];
334
+ onWarn?: (msg: string) => void;
335
+ };
336
+
337
+ /**
338
+ * Whole-file gate from frontmatter `requires_setting` / `requires` (AND if both present).
339
+ */
340
+ export function rulePassesFrontmatterGate(
341
+ attrs: Record<string, string>,
342
+ opts: FrontmatterGateOpts,
343
+ ): boolean {
344
+ const requiresSetting = attrs.requires_setting?.trim();
345
+ const requires = attrs.requires?.trim();
346
+ if (!requiresSetting && !requires) return true;
347
+
348
+ if (requiresSetting) {
349
+ if (opts.knownKeys && !opts.knownKeys.includes(requiresSetting)) {
350
+ opts.onWarn?.(`unknown requires_setting key: ${requiresSetting}`);
351
+ return false;
352
+ }
353
+ if (!isSettingTruthy(opts.resolveSetting(requiresSetting))) {
354
+ return false;
355
+ }
356
+ }
357
+
358
+ if (requires) {
359
+ const pred = opts.featurePredicates[requires];
360
+ if (!pred) {
361
+ opts.onWarn?.(`unknown feature predicate: ${requires}`);
362
+ return false;
363
+ }
364
+ try {
365
+ if (pred() !== true) {
366
+ return false;
367
+ }
368
+ } catch (e: any) {
369
+ opts.onWarn?.(`feature predicate ${requires} threw: ${e?.message ?? String(e)}`);
370
+ return false;
371
+ }
372
+ }
373
+
374
+ return true;
375
+ }
376
+
377
+ export type AssembleRuleTextOpts = {
378
+ resolve: (key: string) => unknown;
379
+ knownKeys: string[];
380
+ featurePredicates: Record<string, () => boolean>;
381
+ reviewLoopMaxCycles: number;
382
+ onWarn?: (msg: string) => void;
383
+ onSkip?: (info: { attrs: Record<string, string>; index: number }) => void;
384
+ onInclude?: (info: { attrs: Record<string, string>; index: number }) => void;
385
+ };
386
+
387
+ /**
388
+ * Pure rule assembly: per-file frontmatter gate → per-file conditionals →
389
+ * join → placeholder substitution.
390
+ *
391
+ * Conditionals run per body so an open `{{IF}}` in one file cannot close in
392
+ * another and strip intervening package rules.
393
+ */
394
+ export function assembleRuleText(contents: string[], opts: AssembleRuleTextOpts): string {
395
+ const bodies: string[] = [];
396
+ const condOpts: ConditionalEvalOpts = {
397
+ knownKeys: opts.knownKeys,
398
+ featurePredicates: opts.featurePredicates,
399
+ onWarn: opts.onWarn,
400
+ };
401
+ for (let index = 0; index < contents.length; index++) {
402
+ const raw = contents[index];
403
+ const { attrs, body } = parseRuleFrontmatter(raw);
404
+ if (!rulePassesFrontmatterGate(attrs, {
405
+ resolveSetting: opts.resolve,
406
+ featurePredicates: opts.featurePredicates,
407
+ knownKeys: opts.knownKeys,
408
+ onWarn: opts.onWarn,
409
+ })) {
410
+ opts.onSkip?.({ attrs, index });
411
+ continue;
412
+ }
413
+ opts.onInclude?.({ attrs, index });
414
+ // Evaluate conditionals per file before join (no cross-file IF matching).
415
+ bodies.push(evaluateConditionalBlocks(body, opts.resolve, condOpts));
416
+ }
417
+ const joined = bodies.join("\n\n");
418
+ // Safety net: collapse table-row blank lines across join boundaries too.
419
+ const collapsed = collapseBlankLinesBetweenTableRows(joined);
420
+ return substituteRulePlaceholders(collapsed, { reviewLoopMaxCycles: opts.reviewLoopMaxCycles });
421
+ }