pi-crew 0.10.5 → 0.10.6

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 (88) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/agents/analyst.md +37 -2
  3. package/agents/cold-verifier.md +10 -1
  4. package/agents/councillor-critic.md +39 -0
  5. package/agents/councillor-pragmatist.md +39 -0
  6. package/agents/councillor-skeptic.md +41 -0
  7. package/agents/critic.md +40 -2
  8. package/agents/designer.md +58 -0
  9. package/agents/executor.md +39 -2
  10. package/agents/explorer.md +38 -2
  11. package/agents/librarian.md +49 -0
  12. package/agents/oracle.md +54 -0
  13. package/agents/orchestrator.md +48 -0
  14. package/agents/planner.md +41 -2
  15. package/agents/reviewer.md +39 -2
  16. package/agents/security-reviewer.md +43 -2
  17. package/agents/test-engineer.md +48 -2
  18. package/agents/verifier.md +14 -1
  19. package/agents/writer.md +32 -2
  20. package/dist/index.mjs +520 -74
  21. package/package.json +1 -1
  22. package/skills/async-worker-recovery/SKILL.md +4 -1
  23. package/skills/child-pi-spawning/SKILL.md +4 -1
  24. package/skills/context-artifact-hygiene/SKILL.md +4 -1
  25. package/skills/council/SKILL.md +24 -45
  26. package/skills/delegation-patterns/SKILL.md +18 -1
  27. package/skills/distill-persona/SKILL.md +4 -1
  28. package/skills/distill-software/SKILL.md +4 -1
  29. package/skills/event-log-tracing/SKILL.md +4 -1
  30. package/skills/git-master/SKILL.md +4 -1
  31. package/skills/iterative-audit/SKILL.md +4 -1
  32. package/skills/live-agent-lifecycle/SKILL.md +4 -1
  33. package/skills/mailbox-interactive/SKILL.md +4 -1
  34. package/skills/model-routing-context/SKILL.md +10 -1
  35. package/skills/multi-perspective-review/SKILL.md +18 -1
  36. package/skills/observability-reliability/SKILL.md +4 -1
  37. package/skills/orchestration/SKILL.md +18 -1
  38. package/skills/ownership-session-security/SKILL.md +4 -1
  39. package/skills/pi-extension-lifecycle/SKILL.md +4 -1
  40. package/skills/post-mortem/SKILL.md +4 -1
  41. package/skills/read-only-explorer/SKILL.md +4 -1
  42. package/skills/real-test-pi-crew/SKILL.md +165 -12
  43. package/skills/requirements-to-task-packet/SKILL.md +10 -1
  44. package/skills/research/SKILL.md +4 -1
  45. package/skills/resource-discovery-config/SKILL.md +10 -1
  46. package/skills/runtime-state-reader/SKILL.md +4 -1
  47. package/skills/safe-bash/SKILL.md +4 -1
  48. package/skills/scrutinize/SKILL.md +24 -1
  49. package/skills/secure-agent-orchestration-review/SKILL.md +4 -1
  50. package/skills/state-mutation-locking/SKILL.md +4 -1
  51. package/skills/systematic-debugging/SKILL.md +4 -1
  52. package/skills/verification-before-done/SKILL.md +18 -1
  53. package/skills/widget-rendering/SKILL.md +4 -1
  54. package/skills/workspace-isolation/SKILL.md +4 -1
  55. package/skills/worktree-isolation/SKILL.md +4 -1
  56. package/src/config/config-validation.ts +1 -0
  57. package/src/config/types.ts +8 -0
  58. package/src/errors.ts +1 -1
  59. package/src/extension/context-status-injection.ts +2 -2
  60. package/src/extension/knowledge-injection.ts +19 -7
  61. package/src/extension/post-init-skill-check.ts +32 -0
  62. package/src/extension/register.ts +9 -1
  63. package/src/extension/registration/hook-registration.ts +20 -3
  64. package/src/extension/registration/tool-loop-guard.ts +243 -0
  65. package/src/extension/team-tool/handle-settings.ts +10 -0
  66. package/src/extension/team-tool/run.ts +42 -1
  67. package/src/extension/team-tool-types.ts +6 -0
  68. package/src/prompt/prompt-runtime.ts +25 -6
  69. package/src/runtime/async-runner.ts +75 -11
  70. package/src/runtime/background-runner.ts +41 -0
  71. package/src/runtime/broker/crew-broker-client.ts +45 -2
  72. package/src/runtime/broker/crew-broker.ts +22 -27
  73. package/src/runtime/broker/protocol/request-parsers.ts +10 -2
  74. package/src/runtime/broker/stdin-handshake.ts +87 -0
  75. package/src/runtime/broker/wait-push.ts +45 -0
  76. package/src/runtime/detached-run-results.ts +25 -1
  77. package/src/runtime/foreground-watchdog.ts +24 -5
  78. package/src/runtime/live-session/live-session-runtime.ts +1 -1
  79. package/src/runtime/model/model-scope.ts +2 -2
  80. package/src/runtime/run-tracker.ts +65 -11
  81. package/src/runtime/skill-instructions.ts +20 -4
  82. package/src/runtime/task-runner/child-executor.ts +1 -1
  83. package/src/runtime/task-runner/prompt-builder.ts +22 -9
  84. package/src/schema/config-schema.ts +1 -0
  85. package/src/skills/discover-skills.ts +2 -2
  86. package/src/ui/settings-overlay.ts +40 -0
  87. package/src/utils/frontmatter.ts +7 -1
  88. package/src/utils/ndjson.ts +9 -1
@@ -9,6 +9,19 @@ export interface RunWaitResult {
9
9
  /** True when the waiter was released early by `detachRunPromise` while the
10
10
  * run itself keeps executing (see that function). */
11
11
  detached?: boolean;
12
+ /** F1 (2026-09-12 live battery): set when the waiter was released early
13
+ * because a task PARKED on `ask` — the broker pushes this via
14
+ * `resolveRunPromise` so the sync caller's tool call returns with the
15
+ * question instead of blocking until the watchdog kills the worker. The
16
+ * run keeps executing; the leader answers via `team action='respond'`
17
+ * then re-blocks via `team action='wait'`. */
18
+ waiting?: {
19
+ taskId: string;
20
+ questionId: string;
21
+ question: string;
22
+ deadline: number;
23
+ options?: string[];
24
+ };
12
25
  }
13
26
 
14
27
  export interface ActiveRunPromise {
@@ -30,7 +43,22 @@ const activeRunPromises = new Map<string, ActiveRunPromise>();
30
43
  */
31
44
  const detachRequests = new Set<string>();
32
45
 
46
+ /** F1 tombstones (2026-09-12): resolveRunPromise deletes the live entry after
47
+ * resolving it, so a register+resolve landing BETWEEN two poll ticks of a
48
+ * slow-path waiter orphaned the payload (the waiter held no reference to the
49
+ * entry). A waiter on the polling path consumes the tombstone instead.
50
+ * Bounded — oldest evicted past the limit (detached runs never wait). */
51
+ const resolvedRunResults = new Map<string, RunWaitResult>();
52
+ const RESOLVED_TOMBSTONE_LIMIT = 32;
53
+
33
54
  export function registerRunPromise(runId: string): ActiveRunPromise {
55
+ // Idempotent (F1 live-probe fix, 2026-09-12): run.ts pre-registers BEFORE
56
+ // startForegroundRun so the waitForRun that runs immediately after can hit
57
+ // the medium path; executeTeamRunCore's later `void registerRunPromise(...)`
58
+ // must NOT overwrite the entry (an overwrite strands waiters holding the
59
+ // old promise — the waiting-push would resolve a promise nobody awaits).
60
+ const existing = activeRunPromises.get(runId);
61
+ if (existing) return existing;
34
62
  detachRequests.delete(runId);
35
63
  let resolve!: (value: RunWaitResult) => void;
36
64
  let reject!: (reason: unknown) => void;
@@ -83,6 +111,13 @@ export function resolveRunPromise(runId: string, result: RunWaitResult): void {
83
111
  entry.resolve(result);
84
112
  activeRunPromises.delete(runId);
85
113
  }
114
+ // F1 tombstone: a slow-path waiter (register/await race, or register+resolve
115
+ // between two ticks) must still see the push. Cheap Map.set; bounded above.
116
+ resolvedRunResults.set(runId, result);
117
+ if (resolvedRunResults.size > RESOLVED_TOMBSTONE_LIMIT) {
118
+ const oldest = resolvedRunResults.keys().next().value;
119
+ if (oldest !== undefined) resolvedRunResults.delete(oldest);
120
+ }
86
121
  }
87
122
 
88
123
  export function rejectRunPromise(runId: string, reason: unknown): void {
@@ -93,6 +128,17 @@ export function rejectRunPromise(runId: string, reason: unknown): void {
93
128
  }
94
129
  }
95
130
 
131
+ function raceRunPromise(entry: ActiveRunPromise, timeoutMs: number, deadline: number): Promise<RunWaitResult> {
132
+ let timer: ReturnType<typeof setTimeout> | undefined;
133
+ const remaining = Math.max(0, deadline - Date.now());
134
+ const timeoutPromise = new Promise<never>((_, reject) => {
135
+ timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), remaining);
136
+ });
137
+ return Promise.race([entry.promise, timeoutPromise]).finally(() => {
138
+ if (timer) clearTimeout(timer);
139
+ });
140
+ }
141
+
96
142
  /**
97
143
  * Wait for a team run to reach a terminal status.
98
144
  * - If the run is already finished on disk, returns immediately.
@@ -122,17 +168,7 @@ export async function waitForRun(
122
168
 
123
169
  // Medium path: foreground promise registered in this process
124
170
  const entry = activeRunPromises.get(runId);
125
- if (entry) {
126
- let timer: ReturnType<typeof setTimeout> | undefined;
127
- const timeoutPromise = new Promise<never>((_, reject) => {
128
- timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), timeoutMs);
129
- });
130
- try {
131
- return await Promise.race([entry.promise, timeoutPromise]);
132
- } finally {
133
- if (timer) clearTimeout(timer);
134
- }
135
- }
171
+ if (entry) return await raceRunPromise(entry, timeoutMs, deadline);
136
172
 
137
173
  // Slow path: background run — poll with exponential backoff capped at pollIntervalMs.
138
174
  // This path is ALSO taken by a foreground run whose executeTeamRun has not
@@ -145,6 +181,14 @@ export async function waitForRun(
145
181
  const current = loadRunManifestById(cwd, runId);
146
182
  if (current) return { ...current, detached: true };
147
183
  }
184
+ // F1 live-probe fix (2026-09-12): a foreground promise may appear AFTER
185
+ // this waiter started (register/await race — the waiting-push from the
186
+ // broker resolves the ENTRY, which the polling loop would otherwise
187
+ // never observe). Re-check each tick and switch to the promise path with
188
+ // the REMAINING budget; evidence team_20260912053049 (parked 05:31:14,
189
+ // push no-op'd, waiter polled until the watchdog killed the worker).
190
+ const entryNow = activeRunPromises.get(runId);
191
+ if (entryNow) return await raceRunPromise(entryNow, timeoutMs, deadline);
148
192
  if (attempt === 0) {
149
193
  // Early exit: if the run directory doesn't exist, don't waste time polling.
150
194
  // Resolve through createRunPaths (scopeBaseRoot) so the probe matches where
@@ -163,6 +207,15 @@ export async function waitForRun(
163
207
  if (fresh && isFinishedRunStatus(fresh.manifest.status)) {
164
208
  return fresh;
165
209
  }
210
+ // F1 tombstone: a push that resolved (and evicted) the live entry between
211
+ // ticks lands here — consume it so the leader sees the question, not a
212
+ // 600s block. Terminal-on-disk above still wins (a finished run outranks
213
+ // a stale waiting payload).
214
+ const tombstone = resolvedRunResults.get(runId);
215
+ if (tombstone) {
216
+ resolvedRunResults.delete(runId);
217
+ return tombstone;
218
+ }
166
219
  const delay = Math.min(pollIntervalMs, 50 * 2 ** Math.min(attempt, 6)); // max ~3.2s
167
220
  await new Promise((r) => setTimeout(r, delay));
168
221
  attempt++;
@@ -177,6 +230,7 @@ export function hasActiveRunPromise(runId: string): boolean {
177
230
 
178
231
  export function clearRunPromisesForTest(): void {
179
232
  detachRequests.clear();
233
+ resolvedRunResults.clear();
180
234
  for (const entry of activeRunPromises.values()) {
181
235
  entry.reject(new Error("Cleared by test"));
182
236
  }
@@ -1,14 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import type { AgentConfig } from "../agents/agent-config.ts";
5
4
  import type { TeamRole } from "../teams/team-config.ts";
6
5
  import { logInternalError } from "../utils/internal-error.ts";
6
+ import { packageRoot } from "../utils/paths.ts";
7
7
  import { isSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
8
8
  import type { WorkflowStep } from "../workflows/workflow-config.ts";
9
9
  import { CONFIDENCE_THRESHOLDS, getWeightedSkillsForRole, registerSkillEffectivenessHooks } from "./skill-effectiveness.ts";
10
10
 
11
- const PACKAGE_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
11
+ const PACKAGE_SKILLS_DIR = path.join(packageRoot(), "skills");
12
12
 
13
13
  import * as os from "node:os";
14
14
  // peer-dep.ts resolves @earendil-works/pi-coding-agent robustly across install
@@ -121,8 +121,24 @@ function collectTaskSkillNames(input: ResolveTaskSkillsInput | undefined): strin
121
121
  if (input.agent?.skills?.length) names.push(...input.agent.skills);
122
122
  if (Array.isArray(input.teamRole?.skills)) names.push(...input.teamRole.skills);
123
123
  if (Array.isArray(input.step?.skills)) names.push(...input.step.skills);
124
- if (Array.isArray(input.override)) names.push(...input.override);
125
- return unique(names);
124
+ // SKILL-HYGIENE-2: support wildcard (`*`) and denylist (`!name`) syntax in override.
125
+ // - `*` is a marker (no-op; defaults already included via defaultSkillsForRole).
126
+ // - `!name` removes `name` from the final selection.
127
+ // - Other names are added (existing additive behavior).
128
+ const denylist = new Set<string>();
129
+ if (Array.isArray(input.override)) {
130
+ for (const item of input.override) {
131
+ if (item === "*") {
132
+ // wildcard marker; defaults already in names via defaultSkillsForRole
133
+ } else if (item.startsWith("!")) {
134
+ denylist.add(item.slice(1));
135
+ } else {
136
+ names.push(item);
137
+ }
138
+ }
139
+ }
140
+ if (denylist.size === 0) return unique(names);
141
+ return unique(names).filter((n) => !denylist.has(n));
126
142
  }
127
143
 
128
144
  export function resolveTaskSkillNames(input: ResolveTaskSkillsInput): string[] {
@@ -107,7 +107,7 @@ async function appendBackgroundLogAsync(bgLogPath: string, eventLine: string): P
107
107
 
108
108
  /**
109
109
  * F7: resolve the enabledModels allowlist for the child-process spawn path,
110
- * but only if `runtime.reliability.scopeModels` is ON. Returns [] (no-op)
110
+ * but only if `reliability.scopeModels` is ON. Returns [] (no-op)
111
111
  * when the toggle is off or the allowlist is empty. Best-effort: any failure
112
112
  * to read config or the allowlist silently disables the gate so spawn is
113
113
  * never blocked by a misconfiguration.
@@ -35,10 +35,11 @@ function readOnlyRoleInstructions(role: string): string {
35
35
  ].join("\n");
36
36
  }
37
37
 
38
- export function coordinationBridgeInstructions(task: TeamTaskState): string {
38
+ export function coordinationBridgeInstructions(task: TeamTaskState, opts?: { includeMailboxTarget?: boolean }): string {
39
+ const includeMailboxTarget = opts?.includeMailboxTarget ?? true;
39
40
  return [
40
41
  "# Crew Coordination Channel",
41
- `Mailbox target for this task: ${task.id}`,
42
+ ...(includeMailboxTarget ? [`Mailbox target for this task: ${task.id}`] : []),
42
43
  "Use the run mailbox contract for coordination with the leader/orchestrator:",
43
44
  "- If blocked or uncertain, report the blocker in your final result and, when mailbox tools/API are available, send an inbox/outbox message addressed to the leader.",
44
45
  "- Never guess implementation details that materially affect decisions. If the `ask` tool is available and you need a clarification, a decision, or a missing requirement before you can proceed safely, call `ask` and wait — a parked question is cheaper than a wrong build.",
@@ -262,7 +263,10 @@ export async function renderTaskPrompt(
262
263
  // computation for parallel siblings in the same batch.
263
264
  const stableComponents = precomputedStableComponents ?? (await computeStablePrefixComponents(manifest, step, task, agent));
264
265
 
265
- // Stable prefix: role instructions, coordination, workspace tree — rarely changes
266
+ // Stable prefix: role instructions, coordination, workspace tree — rarely changes.
267
+ // ARCH-3 (byte-stable worker prefix): per-task values (Task ID, Task cwd, mailbox
268
+ // target) live in dynamicSuffix so siblings sharing a run+role produce a
269
+ // byte-identical prefix and hit provider KV-cache across the batch.
266
270
  const stablePrefix = [
267
271
  "# pi-crew Worker Runtime Context",
268
272
  `Run ID: ${manifest.runId}`,
@@ -271,8 +275,6 @@ export async function renderTaskPrompt(
271
275
  `State root: ${manifest.stateRoot}`,
272
276
  `Artifacts root: ${manifest.artifactsRoot}`,
273
277
  `Events path: ${manifest.eventsPath}`,
274
- `Task ID: ${task.id}`,
275
- `Task cwd: ${task.cwd}`,
276
278
  `Workspace mode: ${manifest.workspaceMode}`,
277
279
  "",
278
280
  "Protocol:",
@@ -280,10 +282,14 @@ export async function renderTaskPrompt(
280
282
  "- Report blockers and verification evidence in the final result.",
281
283
  "- Do not claim completion without evidence.",
282
284
  "- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
285
+ // PROMPT-2 (port of OMO-slim task-rejection, improved phrasing): a
286
+ // universal lane-guard for every role — complements the per-agent reject
287
+ // sections in agents/*.md with a scaffold-level instruction.
288
+ "- If a task falls outside your role, do not attempt partial work. Return a concise rejection to the leader naming the lane that should own it.",
283
289
  "",
284
290
  readOnlyRoleInstructions(task.role),
285
291
  "",
286
- coordinationBridgeInstructions(task),
292
+ coordinationBridgeInstructions(task, { includeMailboxTarget: false }),
287
293
  "",
288
294
  stableComponents.treeBlock,
289
295
  "",
@@ -291,9 +297,13 @@ export async function renderTaskPrompt(
291
297
  "",
292
298
  toolGuidanceBlock(agent),
293
299
  "",
294
- // O4: project knowledge (.crew/knowledge.md) — workers don't load the
295
- // pi-crew extension (spawned with --no-extensions), so before_agent_start
296
- // never fires for them. Inject here so every worker sees project knowledge.
300
+ // O4 (ARCH-2 corrected): project knowledge (.crew/knowledge.md). Builtin
301
+ // workers don't load the pi-crew extension (agents declare no `extensions:`
302
+ // in frontmatter), so before_agent_start knowledge injection doesn't fire
303
+ // for them — and the knowledge-injection hook now early-returns on
304
+ // PI_CREW_KIND=subagent, so even agents that DO declare the extension
305
+ // can't double-inject. This prompt-builder fragment is the single source
306
+ // of worker project knowledge.
297
307
  stableComponents.knowledgeFragment,
298
308
  ]
299
309
  .filter(Boolean)
@@ -301,6 +311,9 @@ export async function renderTaskPrompt(
301
311
 
302
312
  // Dynamic suffix: goal, step, skills, task packet, dependency context, memory — changes per task
303
313
  const dynamicSuffix = [
314
+ `Task ID: ${task.id}`,
315
+ `Task cwd: ${task.cwd}`,
316
+ `Mailbox target: ${task.id}`,
304
317
  `Goal:\n${manifest.goal}`,
305
318
  "",
306
319
  `Step: ${step.id}`,
@@ -270,6 +270,7 @@ export const PiTeamsReliabilityConfigSchema = Type.Object(
270
270
  ambientStatusInjection: Type.Optional(Type.Boolean()),
271
271
  perWriteValidation: Type.Optional(Type.Boolean()),
272
272
  scopeModels: Type.Optional(Type.Boolean()),
273
+ loopGuard: Type.Optional(Type.Boolean()),
273
274
  },
274
275
  { additionalProperties: false },
275
276
  );
@@ -1,15 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  // peer-dep.ts resolves @earendil-works/pi-coding-agent robustly across install
6
5
  // layouts. See src/runtime/peer-dep.ts (split-scope install fix).
7
6
  import { getAgentDir } from "../runtime/peer-dep.ts";
8
7
  import { logInternalError } from "../utils/internal-error.ts";
8
+ import { packageRoot } from "../utils/paths.ts";
9
9
  import { isSafePathId, resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
10
10
  import { parseSkillFrontmatter, type SkillValidationError, validateSkillFrontmatter } from "./validate.ts";
11
11
 
12
- const PACKAGE_SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
12
+ const PACKAGE_SKILLS_DIR = path.join(packageRoot(), "skills");
13
13
 
14
14
  const CACHE_TTL_MS = 30_000; // 30 seconds
15
15
  let cache: { skills: SkillDescriptor[]; cachedAt: number; cwd: string } | null = null;
@@ -304,6 +304,41 @@ const SETTINGS: SettingDef[] = [
304
304
  tab: "advanced",
305
305
  description: "Remove /tmp/pi-crew-* directories after reconciliation (1h age threshold).",
306
306
  },
307
+ {
308
+ id: "reliability.loopGuard",
309
+ label: "Tool Loop Guard",
310
+ type: "boolean",
311
+ tab: "advanced",
312
+ description: "Warn at 3 / block at 5 identical consecutive read-only tool results (ARCH-1).",
313
+ },
314
+ {
315
+ id: "reliability.perWriteValidation",
316
+ label: "Per-Write Validation",
317
+ type: "boolean",
318
+ tab: "advanced",
319
+ description: "Validate state writes as they are made (default on).",
320
+ },
321
+ {
322
+ id: "reliability.ambientStatusInjection",
323
+ label: "Ambient Status Injection",
324
+ type: "boolean",
325
+ tab: "advanced",
326
+ description: "Inject a compact crew-status note into context on every LLM call while runs are in-flight.",
327
+ },
328
+ {
329
+ id: "reliability.forcePreflight",
330
+ label: "Force Preflight (audit override)",
331
+ type: "boolean",
332
+ tab: "advanced",
333
+ description: "Skip usage-threshold BLOCK/WARN preflight. Default false (enforce). Audit/debug only.",
334
+ },
335
+ {
336
+ id: "reliability.scopeModels",
337
+ label: "Scope Models (F7)",
338
+ type: "boolean",
339
+ tab: "advanced",
340
+ description: "Enforce user enabledModels allowlist on subagent model choices. Default false.",
341
+ },
307
342
  {
308
343
  id: "telemetry.enabled",
309
344
  label: "Telemetry",
@@ -359,6 +394,11 @@ const EFFECTIVE_DEFAULTS: Record<string, unknown> = {
359
394
  "reliability.autoRetry": false,
360
395
  "reliability.autoRecover": false,
361
396
  "reliability.cleanupOrphanedTempDirs": true,
397
+ "reliability.loopGuard": true,
398
+ "reliability.perWriteValidation": true,
399
+ "reliability.ambientStatusInjection": true,
400
+ "reliability.forcePreflight": false,
401
+ "reliability.scopeModels": false,
362
402
  "telemetry.enabled": false,
363
403
  "notifications.enabled": false,
364
404
  };
@@ -35,7 +35,13 @@ function parseLines(raw: string): Record<string, string> {
35
35
  const separator = trimmed.indexOf(":");
36
36
  if (separator === -1) continue;
37
37
  const key = trimmed.slice(0, separator).trim();
38
- const value = trimmed.slice(separator + 1).trim();
38
+ let value = trimmed.slice(separator + 1).trim();
39
+ // Strip one pair of symmetric surrounding double quotes so quoted
40
+ // frontmatter values (required by strict YAML when the value contains
41
+ // ": ") behave identically to unquoted ones for every consumer.
42
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
43
+ value = value.slice(1, -1);
44
+ }
39
45
  if (key) frontmatter[key] = value;
40
46
  }
41
47
  return frontmatter;
@@ -17,7 +17,15 @@ export const MAX_BROKER_FRAME_BYTES = 256 * 1024;
17
17
  // BrokerError — typed protocol errors
18
18
  // ============================================================================
19
19
 
20
- export type BrokerErrorCode = "oversize-frame" | "auth" | "protocol" | "timeout" | "close" | "not-implemented" | "rate-limit";
20
+ export type BrokerErrorCode =
21
+ | "oversize-frame"
22
+ | "auth"
23
+ | "protocol"
24
+ | "timeout"
25
+ | "request-timeout"
26
+ | "close"
27
+ | "not-implemented"
28
+ | "rate-limit";
21
29
 
22
30
  export class BrokerError extends Error {
23
31
  readonly code: BrokerErrorCode;