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
package/dist/index.mjs CHANGED
@@ -10606,7 +10606,8 @@ var init_config_schema = __esm({
10606
10606
  forcePreflight: Type.Optional(Type.Boolean()),
10607
10607
  ambientStatusInjection: Type.Optional(Type.Boolean()),
10608
10608
  perWriteValidation: Type.Optional(Type.Boolean()),
10609
- scopeModels: Type.Optional(Type.Boolean())
10609
+ scopeModels: Type.Optional(Type.Boolean()),
10610
+ loopGuard: Type.Optional(Type.Boolean())
10610
10611
  },
10611
10612
  { additionalProperties: false }
10612
10613
  );
@@ -11253,6 +11254,7 @@ function parseReliabilityConfig(value) {
11253
11254
  forcePreflight: parseWithSchema(Type.Boolean(), obj.forcePreflight),
11254
11255
  ambientStatusInjection: parseWithSchema(Type.Boolean(), obj.ambientStatusInjection),
11255
11256
  perWriteValidation: parseWithSchema(Type.Boolean(), obj.perWriteValidation),
11257
+ loopGuard: parseWithSchema(Type.Boolean(), obj.loopGuard),
11256
11258
  scopeModels: parseWithSchema(Type.Boolean(), obj.scopeModels)
11257
11259
  };
11258
11260
  return Object.values(reliability).some((entry) => entry !== void 0) ? reliability : void 0;
@@ -12592,7 +12594,10 @@ function parseLines(raw) {
12592
12594
  const separator = trimmed.indexOf(":");
12593
12595
  if (separator === -1) continue;
12594
12596
  const key = trimmed.slice(0, separator).trim();
12595
- const value = trimmed.slice(separator + 1).trim();
12597
+ let value = trimmed.slice(separator + 1).trim();
12598
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
12599
+ value = value.slice(1, -1);
12600
+ }
12596
12601
  if (key) frontmatter[key] = value;
12597
12602
  }
12598
12603
  return frontmatter;
@@ -15396,7 +15401,7 @@ var init_errors3 = __esm({
15396
15401
  [ErrorCode.EventLogLockTimeout]: "Another process holds the event-log lock. Check for orphaned `.mkdirlock` directories or stale pi-crew processes, then retry.",
15397
15402
  [ErrorCode.DepthLimitExceeded]: "A pipeline/chain exceeded the recursion depth limit, which usually indicates a circular stage dependency. Review step `dependsOn` chains.",
15398
15403
  [ErrorCode.RunStale]: "The worker stopped heartbeating and was treated as a zombie. Re-run the team (resume or fresh); if it recurs, check `runtime.executeWorkers` / system load.",
15399
- [ErrorCode.ModelOutOfScope]: "The requested model is not in your pi `enabledModels` allowlist. Either pick a model listed in `enabledModels` (settings.json) or extend the allowlist. The scope gate is opt-in \u2014 disable `runtime.reliability.scopeModels` to allow any model."
15404
+ [ErrorCode.ModelOutOfScope]: "The requested model is not in your pi `enabledModels` allowlist. Either pick a model listed in `enabledModels` (settings.json) or extend the allowlist. The scope gate is opt-in \u2014 disable `reliability.scopeModels` to allow any model."
15400
15405
  };
15401
15406
  CrewError = class extends Error {
15402
15407
  code;
@@ -17345,10 +17350,10 @@ function dedupeTerminalEvents(events) {
17345
17350
  const seen = /* @__PURE__ */ new Set();
17346
17351
  const output = [];
17347
17352
  for (const event of events) {
17348
- const fingerprint = event.metadata?.fingerprint;
17349
- if (fingerprint && TERMINAL_EVENT_TYPES.has(event.type)) {
17350
- if (seen.has(fingerprint)) continue;
17351
- seen.add(fingerprint);
17353
+ const fingerprint2 = event.metadata?.fingerprint;
17354
+ if (fingerprint2 && TERMINAL_EVENT_TYPES.has(event.type)) {
17355
+ if (seen.has(fingerprint2)) continue;
17356
+ seen.add(fingerprint2);
17352
17357
  }
17353
17358
  output.push(event);
17354
17359
  }
@@ -17692,6 +17697,13 @@ var init_run_event_bus = __esm({
17692
17697
  });
17693
17698
 
17694
17699
  // src/runtime/broker/broker-issuer.ts
17700
+ var broker_issuer_exports = {};
17701
+ __export(broker_issuer_exports, {
17702
+ getActiveBrokerIssuer: () => getActiveBrokerIssuer,
17703
+ getActiveBrokerRevoker: () => getActiveBrokerRevoker,
17704
+ setActiveBrokerIssuer: () => setActiveBrokerIssuer,
17705
+ setActiveBrokerRevoker: () => setActiveBrokerRevoker
17706
+ });
17695
17707
  function setActiveBrokerIssuer(issuer) {
17696
17708
  activeIssuer = issuer;
17697
17709
  }
@@ -29423,8 +29435,21 @@ var init_i18n = __esm({
29423
29435
  });
29424
29436
 
29425
29437
  // src/runtime/run-tracker.ts
29438
+ var run_tracker_exports = {};
29439
+ __export(run_tracker_exports, {
29440
+ clearRunPromisesForTest: () => clearRunPromisesForTest,
29441
+ detachRunPromise: () => detachRunPromise,
29442
+ hasActiveRunPromise: () => hasActiveRunPromise,
29443
+ hasPendingRunDetach: () => hasPendingRunDetach,
29444
+ registerRunPromise: () => registerRunPromise,
29445
+ rejectRunPromise: () => rejectRunPromise,
29446
+ resolveRunPromise: () => resolveRunPromise,
29447
+ waitForRun: () => waitForRun
29448
+ });
29426
29449
  import * as fs49 from "node:fs";
29427
29450
  function registerRunPromise(runId) {
29451
+ const existing = activeRunPromises.get(runId);
29452
+ if (existing) return existing;
29428
29453
  detachRequests.delete(runId);
29429
29454
  let resolve27;
29430
29455
  let reject;
@@ -29436,12 +29461,33 @@ function registerRunPromise(runId) {
29436
29461
  activeRunPromises.set(runId, entry);
29437
29462
  return entry;
29438
29463
  }
29464
+ function detachRunPromise(runId, cwd) {
29465
+ const loaded = loadRunManifestById(cwd, runId);
29466
+ if (!loaded) return false;
29467
+ const entry = activeRunPromises.get(runId);
29468
+ if (entry) {
29469
+ activeRunPromises.delete(runId);
29470
+ detachRequests.delete(runId);
29471
+ entry.resolve({ ...loaded, detached: true });
29472
+ return true;
29473
+ }
29474
+ detachRequests.add(runId);
29475
+ return true;
29476
+ }
29477
+ function hasPendingRunDetach(runId) {
29478
+ return detachRequests.has(runId);
29479
+ }
29439
29480
  function resolveRunPromise(runId, result4) {
29440
29481
  const entry = activeRunPromises.get(runId);
29441
29482
  if (entry) {
29442
29483
  entry.resolve(result4);
29443
29484
  activeRunPromises.delete(runId);
29444
29485
  }
29486
+ resolvedRunResults.set(runId, result4);
29487
+ if (resolvedRunResults.size > RESOLVED_TOMBSTONE_LIMIT) {
29488
+ const oldest = resolvedRunResults.keys().next().value;
29489
+ if (oldest !== void 0) resolvedRunResults.delete(oldest);
29490
+ }
29445
29491
  }
29446
29492
  function rejectRunPromise(runId, reason) {
29447
29493
  const entry = activeRunPromises.get(runId);
@@ -29450,6 +29496,16 @@ function rejectRunPromise(runId, reason) {
29450
29496
  activeRunPromises.delete(runId);
29451
29497
  }
29452
29498
  }
29499
+ function raceRunPromise(entry, timeoutMs, deadline) {
29500
+ let timer;
29501
+ const remaining = Math.max(0, deadline - Date.now());
29502
+ const timeoutPromise = new Promise((_, reject) => {
29503
+ timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), remaining);
29504
+ });
29505
+ return Promise.race([entry.promise, timeoutPromise]).finally(() => {
29506
+ if (timer) clearTimeout(timer);
29507
+ });
29508
+ }
29453
29509
  async function waitForRun(runId, cwd, options = {}) {
29454
29510
  const { timeoutMs = 3e5, pollIntervalMs = 500 } = options;
29455
29511
  const deadline = Date.now() + timeoutMs;
@@ -29462,23 +29518,15 @@ async function waitForRun(runId, cwd, options = {}) {
29462
29518
  return { ...loaded, detached: true };
29463
29519
  }
29464
29520
  const entry = activeRunPromises.get(runId);
29465
- if (entry) {
29466
- let timer;
29467
- const timeoutPromise = new Promise((_, reject) => {
29468
- timer = setTimeout(() => reject(new Error(`waitForRun timed out after ${timeoutMs}ms`)), timeoutMs);
29469
- });
29470
- try {
29471
- return await Promise.race([entry.promise, timeoutPromise]);
29472
- } finally {
29473
- if (timer) clearTimeout(timer);
29474
- }
29475
- }
29521
+ if (entry) return await raceRunPromise(entry, timeoutMs, deadline);
29476
29522
  let attempt = 0;
29477
29523
  while (Date.now() < deadline) {
29478
29524
  if (detachRequests.delete(runId)) {
29479
29525
  const current = loadRunManifestById(cwd, runId);
29480
29526
  if (current) return { ...current, detached: true };
29481
29527
  }
29528
+ const entryNow = activeRunPromises.get(runId);
29529
+ if (entryNow) return await raceRunPromise(entryNow, timeoutMs, deadline);
29482
29530
  if (attempt === 0) {
29483
29531
  const runDir = createRunPaths(cwd, runId).stateRoot;
29484
29532
  if (!fs49.existsSync(runDir)) {
@@ -29489,13 +29537,29 @@ async function waitForRun(runId, cwd, options = {}) {
29489
29537
  if (fresh && isFinishedRunStatus(fresh.manifest.status)) {
29490
29538
  return fresh;
29491
29539
  }
29540
+ const tombstone = resolvedRunResults.get(runId);
29541
+ if (tombstone) {
29542
+ resolvedRunResults.delete(runId);
29543
+ return tombstone;
29544
+ }
29492
29545
  const delay = Math.min(pollIntervalMs, 50 * 2 ** Math.min(attempt, 6));
29493
29546
  await new Promise((r) => setTimeout(r, delay));
29494
29547
  attempt++;
29495
29548
  }
29496
29549
  throw new Error(`waitForRun timed out after ${timeoutMs}ms`);
29497
29550
  }
29498
- var activeRunPromises, detachRequests;
29551
+ function hasActiveRunPromise(runId) {
29552
+ return activeRunPromises.has(runId);
29553
+ }
29554
+ function clearRunPromisesForTest() {
29555
+ detachRequests.clear();
29556
+ resolvedRunResults.clear();
29557
+ for (const entry of activeRunPromises.values()) {
29558
+ entry.reject(new Error("Cleared by test"));
29559
+ }
29560
+ activeRunPromises.clear();
29561
+ }
29562
+ var activeRunPromises, detachRequests, resolvedRunResults, RESOLVED_TOMBSTONE_LIMIT;
29499
29563
  var init_run_tracker = __esm({
29500
29564
  "src/runtime/run-tracker.ts"() {
29501
29565
  "use strict";
@@ -29503,6 +29567,8 @@ var init_run_tracker = __esm({
29503
29567
  init_process_status();
29504
29568
  activeRunPromises = /* @__PURE__ */ new Map();
29505
29569
  detachRequests = /* @__PURE__ */ new Set();
29570
+ resolvedRunResults = /* @__PURE__ */ new Map();
29571
+ RESOLVED_TOMBSTONE_LIMIT = 32;
29506
29572
  }
29507
29573
  });
29508
29574
 
@@ -29874,7 +29940,6 @@ __export(skill_instructions_exports, {
29874
29940
  });
29875
29941
  import * as fs50 from "node:fs";
29876
29942
  import * as path38 from "node:path";
29877
- import { fileURLToPath as fileURLToPath4 } from "node:url";
29878
29943
  import * as os11 from "node:os";
29879
29944
  function isValidSkillName(name) {
29880
29945
  return name.length > 0 && name.length <= MAX_SKILL_NAME_CHARS && isSafePathId(name);
@@ -29912,8 +29977,19 @@ function collectTaskSkillNames(input) {
29912
29977
  if (input.agent?.skills?.length) names.push(...input.agent.skills);
29913
29978
  if (Array.isArray(input.teamRole?.skills)) names.push(...input.teamRole.skills);
29914
29979
  if (Array.isArray(input.step?.skills)) names.push(...input.step.skills);
29915
- if (Array.isArray(input.override)) names.push(...input.override);
29916
- return unique(names);
29980
+ const denylist = /* @__PURE__ */ new Set();
29981
+ if (Array.isArray(input.override)) {
29982
+ for (const item of input.override) {
29983
+ if (item === "*") {
29984
+ } else if (item.startsWith("!")) {
29985
+ denylist.add(item.slice(1));
29986
+ } else {
29987
+ names.push(item);
29988
+ }
29989
+ }
29990
+ }
29991
+ if (denylist.size === 0) return unique(names);
29992
+ return unique(names).filter((n) => !denylist.has(n));
29917
29993
  }
29918
29994
  function resolveTaskSkillNames(input) {
29919
29995
  return collectTaskSkillNames(input).slice(0, MAX_SELECTED_SKILLS);
@@ -30124,10 +30200,11 @@ var init_skill_instructions = __esm({
30124
30200
  "src/runtime/skill-instructions.ts"() {
30125
30201
  "use strict";
30126
30202
  init_internal_error();
30203
+ init_paths();
30127
30204
  init_safe_paths();
30128
30205
  init_skill_effectiveness();
30129
30206
  init_peer_dep();
30130
- PACKAGE_SKILLS_DIR = path38.resolve(path38.dirname(fileURLToPath4(import.meta.url)), "..", "..", "skills");
30207
+ PACKAGE_SKILLS_DIR = path38.join(packageRoot(), "skills");
30131
30208
  MAX_SKILL_CHARS = 1500;
30132
30209
  MAX_TOTAL_CHARS = 6e3;
30133
30210
  MAX_SKILL_NAME_CHARS = 80;
@@ -32305,7 +32382,6 @@ var init_validate = __esm({
32305
32382
  import * as fs54 from "node:fs";
32306
32383
  import * as os12 from "node:os";
32307
32384
  import * as path40 from "node:path";
32308
- import { fileURLToPath as fileURLToPath5 } from "node:url";
32309
32385
  function listSkillDirs(cwd) {
32310
32386
  return [
32311
32387
  { root: PACKAGE_SKILLS_DIR2, source: "package" },
@@ -32423,9 +32499,10 @@ var init_discover_skills = __esm({
32423
32499
  "use strict";
32424
32500
  init_peer_dep();
32425
32501
  init_internal_error();
32502
+ init_paths();
32426
32503
  init_safe_paths();
32427
32504
  init_validate();
32428
- PACKAGE_SKILLS_DIR2 = path40.resolve(path40.dirname(fileURLToPath5(import.meta.url)), "..", "..", "skills");
32505
+ PACKAGE_SKILLS_DIR2 = path40.join(packageRoot(), "skills");
32429
32506
  CACHE_TTL_MS = 3e4;
32430
32507
  cache2 = null;
32431
32508
  lastDiagnostics = [];
@@ -46534,14 +46611,14 @@ var init_protocol = __esm({
46534
46611
  });
46535
46612
 
46536
46613
  // src/runtime/scratchpad/engine.ts
46537
- import { fileURLToPath as fileURLToPath6 } from "node:url";
46614
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
46538
46615
  var SNAPSHOT_MAX_BYTES, GUEST_PATH;
46539
46616
  var init_engine = __esm({
46540
46617
  "src/runtime/scratchpad/engine.ts"() {
46541
46618
  "use strict";
46542
46619
  init_protocol();
46543
46620
  SNAPSHOT_MAX_BYTES = 4 * 1024 * 1024;
46544
- GUEST_PATH = fileURLToPath6(new URL("./guest.ts", import.meta.url));
46621
+ GUEST_PATH = fileURLToPath4(new URL("./guest.ts", import.meta.url));
46545
46622
  }
46546
46623
  });
46547
46624
 
@@ -48535,6 +48612,7 @@ function buildKnowledgeFragment(cwd, query) {
48535
48612
  }
48536
48613
  function registerKnowledgeInjection(pi) {
48537
48614
  pi.on("before_agent_start", (event) => {
48615
+ if (process.env.PI_CREW_KIND === "subagent") return;
48538
48616
  const options = event.systemPromptOptions ?? {};
48539
48617
  const cwd = typeof options.cwd === "string" ? options.cwd : process.cwd();
48540
48618
  const fragment = buildKnowledgeFragment(cwd);
@@ -49079,10 +49157,11 @@ function readOnlyRoleInstructions(role) {
49079
49157
  "- Your final RESULT TEXT is persisted automatically by the runner (as a result artifact and, if the step declares `output:`, to a shared file). To deliver a plan, report, or findings, EMIT THEM AS TEXT in your final result \u2014 do NOT try to write a file yourself."
49080
49158
  ].join("\n");
49081
49159
  }
49082
- function coordinationBridgeInstructions(task) {
49160
+ function coordinationBridgeInstructions(task, opts) {
49161
+ const includeMailboxTarget = opts?.includeMailboxTarget ?? true;
49083
49162
  return [
49084
49163
  "# Crew Coordination Channel",
49085
- `Mailbox target for this task: ${task.id}`,
49164
+ ...includeMailboxTarget ? [`Mailbox target for this task: ${task.id}`] : [],
49086
49165
  "Use the run mailbox contract for coordination with the leader/orchestrator:",
49087
49166
  "- 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.",
49088
49167
  "- 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 \u2014 a parked question is cheaper than a wrong build.",
@@ -49212,8 +49291,6 @@ async function renderTaskPrompt(manifest, step, task, agent, skillBlock = "", pr
49212
49291
  `State root: ${manifest.stateRoot}`,
49213
49292
  `Artifacts root: ${manifest.artifactsRoot}`,
49214
49293
  `Events path: ${manifest.eventsPath}`,
49215
- `Task ID: ${task.id}`,
49216
- `Task cwd: ${task.cwd}`,
49217
49294
  `Workspace mode: ${manifest.workspaceMode}`,
49218
49295
  "",
49219
49296
  "Protocol:",
@@ -49221,10 +49298,14 @@ async function renderTaskPrompt(manifest, step, task, agent, skillBlock = "", pr
49221
49298
  "- Report blockers and verification evidence in the final result.",
49222
49299
  "- Do not claim completion without evidence.",
49223
49300
  "- Follow the Task Packet contract below; escalate if any contract field is impossible to satisfy.",
49301
+ // PROMPT-2 (port of OMO-slim task-rejection, improved phrasing): a
49302
+ // universal lane-guard for every role — complements the per-agent reject
49303
+ // sections in agents/*.md with a scaffold-level instruction.
49304
+ "- 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.",
49224
49305
  "",
49225
49306
  readOnlyRoleInstructions(task.role),
49226
49307
  "",
49227
- coordinationBridgeInstructions(task),
49308
+ coordinationBridgeInstructions(task, { includeMailboxTarget: false }),
49228
49309
  "",
49229
49310
  stableComponents.treeBlock,
49230
49311
  "",
@@ -49232,12 +49313,19 @@ async function renderTaskPrompt(manifest, step, task, agent, skillBlock = "", pr
49232
49313
  "",
49233
49314
  toolGuidanceBlock(agent),
49234
49315
  "",
49235
- // O4: project knowledge (.crew/knowledge.md) — workers don't load the
49236
- // pi-crew extension (spawned with --no-extensions), so before_agent_start
49237
- // never fires for them. Inject here so every worker sees project knowledge.
49316
+ // O4 (ARCH-2 corrected): project knowledge (.crew/knowledge.md). Builtin
49317
+ // workers don't load the pi-crew extension (agents declare no `extensions:`
49318
+ // in frontmatter), so before_agent_start knowledge injection doesn't fire
49319
+ // for them — and the knowledge-injection hook now early-returns on
49320
+ // PI_CREW_KIND=subagent, so even agents that DO declare the extension
49321
+ // can't double-inject. This prompt-builder fragment is the single source
49322
+ // of worker project knowledge.
49238
49323
  stableComponents.knowledgeFragment
49239
49324
  ].filter(Boolean).join("\n");
49240
49325
  const dynamicSuffix = [
49326
+ `Task ID: ${task.id}`,
49327
+ `Task cwd: ${task.cwd}`,
49328
+ `Mailbox target: ${task.id}`,
49241
49329
  `Goal:
49242
49330
  ${manifest.goal}`,
49243
49331
  "",
@@ -56505,6 +56593,11 @@ var init_handle_settings = __esm({
56505
56593
  "reliability.autoRetry": false,
56506
56594
  "reliability.autoRecover": false,
56507
56595
  "reliability.cleanupOrphanedTempDirs": true,
56596
+ "reliability.loopGuard": true,
56597
+ "reliability.perWriteValidation": true,
56598
+ "reliability.ambientStatusInjection": true,
56599
+ "reliability.forcePreflight": false,
56600
+ "reliability.scopeModels": false,
56508
56601
  "telemetry.enabled": false,
56509
56602
  "notifications.enabled": false
56510
56603
  };
@@ -56584,6 +56677,11 @@ var init_handle_settings = __esm({
56584
56677
  "reliability.autoRetry",
56585
56678
  "reliability.autoRecover",
56586
56679
  "reliability.cleanupOrphanedTempDirs",
56680
+ "reliability.loopGuard",
56681
+ "reliability.perWriteValidation",
56682
+ "reliability.ambientStatusInjection",
56683
+ "reliability.forcePreflight",
56684
+ "reliability.scopeModels",
56587
56685
  "reliability.deadletterThreshold",
56588
56686
  "reliability.retryPolicy.maxAttempts",
56589
56687
  "reliability.retryPolicy.backoffMs",
@@ -57252,15 +57350,13 @@ import { spawn as spawn6 } from "node:child_process";
57252
57350
  import * as fs89 from "node:fs";
57253
57351
  import { createRequire as createRequire6 } from "node:module";
57254
57352
  import * as path69 from "node:path";
57255
- import { fileURLToPath as fileURLToPath7, pathToFileURL as pathToFileURL2 } from "node:url";
57256
- function packageRootFromRuntime() {
57257
- return path69.resolve(path69.dirname(fileURLToPath7(import.meta.url)), "..", "..");
57258
- }
57353
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
57259
57354
  function jitiRegisterPathFromPackageJson(packageJsonPath) {
57260
57355
  return path69.join(path69.dirname(packageJsonPath), "lib", "jiti-register.mjs");
57261
57356
  }
57262
- function resolveJitiRegisterPath(packageRoot2 = packageRootFromRuntime(), exists = fs89.existsSync) {
57263
- let current = path69.resolve(packageRoot2);
57357
+ function resolveJitiRegisterPath(pkgRoot, exists = fs89.existsSync) {
57358
+ const effectiveRoot = pkgRoot ?? packageRoot();
57359
+ let current = path69.resolve(effectiveRoot);
57264
57360
  const root = path69.parse(current).root;
57265
57361
  while (true) {
57266
57362
  const candidate = path69.join(current, "node_modules", "jiti", "lib", "jiti-register.mjs");
@@ -57313,7 +57409,7 @@ function buildLoaderUnavailableMessage(searchedFrom) {
57313
57409
  }
57314
57410
  function getBackgroundRunnerCommand(runnerPath, cwd, runId, loaderInput = resolveTypeScriptLoader(), reportDirectory) {
57315
57411
  const loader = normalizeLoaderInput(loaderInput);
57316
- if (!loader) throw new Error(buildLoaderUnavailableMessage(packageRootFromRuntime()));
57412
+ if (!loader) throw new Error(buildLoaderUnavailableMessage(packageRoot()));
57317
57413
  const memoryLimit = "--max-old-space-size=512";
57318
57414
  const reportOn = !(getCrewEnv("PI_CREW_BG_REPORT_ON_FATAL") === "0" || getCrewEnv("PI_TEAMS_BG_REPORT_ON_FATAL") === "0");
57319
57415
  const reportDir = reportDirectory ?? path69.dirname(runnerPath);
@@ -57343,6 +57439,10 @@ function getBackgroundRunnerCommand(runnerPath, cwd, runId, loaderInput = resolv
57343
57439
  function buildBackgroundRunnerEnv(env) {
57344
57440
  return { ...env, PI_CREW_ASYNC_RUN: "1" };
57345
57441
  }
57442
+ function buildBrokerStdinLine(runId, socketPath, tasks) {
57443
+ return `${JSON.stringify({ v: 2, runId, socketPath, tasks })}
57444
+ `;
57445
+ }
57346
57446
  async function spawnBackgroundTeamRun(manifest) {
57347
57447
  const runnerPath = path69.join(packageRoot(), "src", "runtime", "background-runner.ts");
57348
57448
  const logPath = path69.join(manifest.stateRoot, "background.log");
@@ -57354,7 +57454,7 @@ async function spawnBackgroundTeamRun(manifest) {
57354
57454
  const childEnv = buildBackgroundRunnerEnv(peerDepDir ? { ...filteredEnv, [PEER_DEP_DIR_ENV]: peerDepDir } : filteredEnv);
57355
57455
  const loader = resolveTypeScriptLoader();
57356
57456
  if (!loader) {
57357
- const message = buildLoaderUnavailableMessage(packageRootFromRuntime());
57457
+ const message = buildLoaderUnavailableMessage(packageRoot());
57358
57458
  await appendEventAsync(manifest.eventsPath, {
57359
57459
  type: "async.failed",
57360
57460
  runId: manifest.runId,
@@ -57369,11 +57469,49 @@ async function spawnBackgroundTeamRun(manifest) {
57369
57469
  cwd: manifest.cwd,
57370
57470
  detached: true,
57371
57471
  setsid: true,
57372
- stdio: ["ignore", "pipe", "pipe"],
57472
+ // F4 (2026-09-12 live battery): stdin is a PIPE — broker credentials
57473
+ // travel heap → pipe → heap right after spawn (see below). Previously
57474
+ // "ignore", which (together with the env allowlist and the missing
57475
+ // runner-side issuer) left EVERY async worker broker-less: ask/message
57476
+ // fell back to "proceed with best judgment" silently.
57477
+ stdio: ["pipe", "pipe", "pipe"],
57373
57478
  env: childEnv,
57374
57479
  windowsHide: true
57375
57480
  };
57376
57481
  const child = spawn6(process.execPath, command.args, spawnOpts);
57482
+ try {
57483
+ let line4;
57484
+ const { getActiveBrokerIssuer: getActiveBrokerIssuer2 } = await Promise.resolve().then(() => (init_broker_issuer(), broker_issuer_exports));
57485
+ const issuer = getActiveBrokerIssuer2();
57486
+ if (issuer) {
57487
+ const { loadRunManifestByIdAsync: loadRunManifestByIdAsync2 } = await Promise.resolve().then(() => (init_state_store(), state_store_exports));
57488
+ const loaded = await loadRunManifestByIdAsync2(manifest.cwd, manifest.runId);
57489
+ const runTasks = loaded?.tasks ?? [];
57490
+ const tasks = {};
57491
+ let socketPath;
57492
+ for (const task of runTasks) {
57493
+ if (!task?.id) continue;
57494
+ const creds = await issuer(manifest.runId, task.id);
57495
+ if (creds) {
57496
+ tasks[task.id] = creds.token;
57497
+ socketPath ??= creds.socketPath;
57498
+ }
57499
+ }
57500
+ if (socketPath && Object.keys(tasks).length > 0) {
57501
+ line4 = buildBrokerStdinLine(manifest.runId, socketPath, tasks);
57502
+ child.stdin?.write(line4);
57503
+ } else {
57504
+ child.stdin?.write("\n");
57505
+ }
57506
+ } else {
57507
+ child.stdin?.write("\n");
57508
+ }
57509
+ } catch {
57510
+ }
57511
+ try {
57512
+ child.stdin?.end();
57513
+ } catch {
57514
+ }
57377
57515
  child.stdout?.destroy();
57378
57516
  const STDERR_CAPTURE_LIMIT = 256 * 1024;
57379
57517
  const stderrChunks = [];
@@ -67218,7 +67356,7 @@ __export(team_runner_exports, {
67218
67356
  import { spawn as spawn7 } from "node:child_process";
67219
67357
  import * as fs104 from "node:fs";
67220
67358
  import * as path83 from "node:path";
67221
- import { fileURLToPath as fileURLToPath8 } from "node:url";
67359
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
67222
67360
  function startTeamRunHeartbeat(stateRoot, runId) {
67223
67361
  const heartbeatPath = path83.join(stateRoot, "heartbeat.json");
67224
67362
  const writeHeartbeat = () => {
@@ -67245,8 +67383,8 @@ function startTeamRunHeartbeat(stateRoot, runId) {
67245
67383
  function perfScriptPath(scriptName) {
67246
67384
  try {
67247
67385
  const candidates = [
67248
- fileURLToPath8(new URL(`../../scripts/${scriptName}`, import.meta.url)),
67249
- fileURLToPath8(new URL(`../scripts/${scriptName}`, import.meta.url))
67386
+ fileURLToPath5(new URL(`../../scripts/${scriptName}`, import.meta.url)),
67387
+ fileURLToPath5(new URL(`../scripts/${scriptName}`, import.meta.url))
67250
67388
  ];
67251
67389
  return candidates.find((p) => fs104.existsSync(p));
67252
67390
  } catch {
@@ -71699,6 +71837,7 @@ ${dwfResult.manifest.summary ?? ""}`,
71699
71837
  const executeWorkers = runtime.kind !== "scaffold";
71700
71838
  if (executeWorkers && ctx.startForegroundRun) {
71701
71839
  const fgDeadline = resolveRunDeadline(ctx, params, executedConfig);
71840
+ registerRunPromise(updatedManifest.runId);
71702
71841
  ctx.onRunStarted?.(updatedManifest.runId);
71703
71842
  const fgSignal = fgDeadline.signal;
71704
71843
  let fgAbortListener;
@@ -71746,6 +71885,36 @@ ${dwfResult.manifest.summary ?? ""}`,
71746
71885
  }, updatedManifest.runId);
71747
71886
  try {
71748
71887
  const completed = await waitForRun(updatedManifest.runId, resolvedCtx.cwd, { timeoutMs: fgDeadline.deadlineMs });
71888
+ if (completed.waiting) {
71889
+ const w = completed.waiting;
71890
+ const secondsLeft = Math.max(0, Math.round((w.deadline - Date.now()) / 1e3));
71891
+ const lines = [
71892
+ `pi-crew run WAITING for your answer: ${updatedManifest.runId}`,
71893
+ `Team: ${team.name} \xB7 Workflow: ${workflow.name}`,
71894
+ `Task ${w.taskId} (${w.questionId.substring(0, 8)}) parked on ask \u2014 ${secondsLeft}s until the deadline (then the worker proceeds with best judgment):`,
71895
+ "",
71896
+ `Q: ${w.question}`
71897
+ ];
71898
+ if (w.options?.length) {
71899
+ lines.push("", "Options:", ...w.options.map((o, i) => ` ${i + 1}. ${o}`));
71900
+ }
71901
+ lines.push(
71902
+ "",
71903
+ "Answer now (run keeps executing):",
71904
+ ` team action='respond' taskId='${w.taskId}' message='<your answer>'`,
71905
+ "then re-block until the run finishes:",
71906
+ ` team action='wait' runId='${updatedManifest.runId}'`
71907
+ );
71908
+ return result(lines.join("\n"), {
71909
+ action: "run",
71910
+ status: "ok",
71911
+ runId: updatedManifest.runId,
71912
+ artifactsRoot: updatedManifest.artifactsRoot,
71913
+ taskId: w.taskId,
71914
+ questionId: w.questionId,
71915
+ waiting: true
71916
+ });
71917
+ }
71749
71918
  if (completed.detached) {
71750
71919
  return result(
71751
71920
  [
@@ -75309,6 +75478,41 @@ var init_settings_overlay = __esm({
75309
75478
  tab: "advanced",
75310
75479
  description: "Remove /tmp/pi-crew-* directories after reconciliation (1h age threshold)."
75311
75480
  },
75481
+ {
75482
+ id: "reliability.loopGuard",
75483
+ label: "Tool Loop Guard",
75484
+ type: "boolean",
75485
+ tab: "advanced",
75486
+ description: "Warn at 3 / block at 5 identical consecutive read-only tool results (ARCH-1)."
75487
+ },
75488
+ {
75489
+ id: "reliability.perWriteValidation",
75490
+ label: "Per-Write Validation",
75491
+ type: "boolean",
75492
+ tab: "advanced",
75493
+ description: "Validate state writes as they are made (default on)."
75494
+ },
75495
+ {
75496
+ id: "reliability.ambientStatusInjection",
75497
+ label: "Ambient Status Injection",
75498
+ type: "boolean",
75499
+ tab: "advanced",
75500
+ description: "Inject a compact crew-status note into context on every LLM call while runs are in-flight."
75501
+ },
75502
+ {
75503
+ id: "reliability.forcePreflight",
75504
+ label: "Force Preflight (audit override)",
75505
+ type: "boolean",
75506
+ tab: "advanced",
75507
+ description: "Skip usage-threshold BLOCK/WARN preflight. Default false (enforce). Audit/debug only."
75508
+ },
75509
+ {
75510
+ id: "reliability.scopeModels",
75511
+ label: "Scope Models (F7)",
75512
+ type: "boolean",
75513
+ tab: "advanced",
75514
+ description: "Enforce user enabledModels allowlist on subagent model choices. Default false."
75515
+ },
75312
75516
  {
75313
75517
  id: "telemetry.enabled",
75314
75518
  label: "Telemetry",
@@ -75359,6 +75563,11 @@ var init_settings_overlay = __esm({
75359
75563
  "reliability.autoRetry": false,
75360
75564
  "reliability.autoRecover": false,
75361
75565
  "reliability.cleanupOrphanedTempDirs": true,
75566
+ "reliability.loopGuard": true,
75567
+ "reliability.perWriteValidation": true,
75568
+ "reliability.ambientStatusInjection": true,
75569
+ "reliability.forcePreflight": false,
75570
+ "reliability.scopeModels": false,
75362
75571
  "telemetry.enabled": false,
75363
75572
  "notifications.enabled": false
75364
75573
  };
@@ -78475,6 +78684,8 @@ function startForegroundWatchdog(opts) {
78475
78684
  const maxMonitorMs = opts.maxMonitorMs ?? DEFAULT_MAX_MONITOR_MS;
78476
78685
  const startTime = Date.now();
78477
78686
  if (activeWatchdogs.has(runId)) return;
78687
+ let consecutiveHungNotices = 0;
78688
+ const HUNG_NOTICE_CAP = 2;
78478
78689
  const check = () => {
78479
78690
  if (Date.now() - startTime > maxMonitorMs) {
78480
78691
  activeWatchdogs.delete(runId);
@@ -78502,13 +78713,23 @@ function startForegroundWatchdog(opts) {
78502
78713
  const now = Date.now();
78503
78714
  if (isLikelyOrphanedActiveRun(manifest, agents, now)) {
78504
78715
  const detail = `status=${manifest.status}, updatedAt=${manifest.updatedAt}, agents=${agents.length}`;
78716
+ consecutiveHungNotices += 1;
78505
78717
  try {
78506
- pi.sendUserMessage(
78507
- `pi-crew watchdog: run ${runId} appears hung (${detail}). Consider running team action='cancel' runId='${runId}' or team action='doctor'.`,
78508
- { deliverAs: "followUp" }
78509
- );
78718
+ if (consecutiveHungNotices <= HUNG_NOTICE_CAP) {
78719
+ pi.sendUserMessage(
78720
+ `pi-crew watchdog: run ${runId} appears hung (${detail}). Consider running team action='cancel' runId='${runId}' or team action='doctor'.`,
78721
+ { deliverAs: "followUp" }
78722
+ );
78723
+ } else if (consecutiveHungNotices === HUNG_NOTICE_CAP + 1) {
78724
+ pi.sendUserMessage(
78725
+ `pi-crew watchdog: run ${runId} is still hung after ${consecutiveHungNotices} checks \u2014 going quiet now. Intervene via team action='cancel' runId='${runId}', team action='doctor', or leave it; a completion notice will still fire.`,
78726
+ { deliverAs: "followUp" }
78727
+ );
78728
+ }
78510
78729
  } catch {
78511
78730
  }
78731
+ } else {
78732
+ consecutiveHungNotices = 0;
78512
78733
  }
78513
78734
  } catch {
78514
78735
  }
@@ -82945,6 +83166,29 @@ function registerCrewMessageRenderers(pi) {
82945
83166
  pi.registerMessageRenderer?.("crew:resume-directive", renderResumeDirective);
82946
83167
  }
82947
83168
 
83169
+ // src/extension/post-init-skill-check.ts
83170
+ init_skill_instructions();
83171
+ async function runPostInitSkillCheck(cwd) {
83172
+ const result4 = renderSkillInstructions({ cwd, role: "executor" });
83173
+ const total = result4.names.length;
83174
+ const missingMatches = result4.block.match(/Skill '([^']+)' was selected but no SKILL\.md file was found/g);
83175
+ const missing = missingMatches ? missingMatches.map((m) => m.match(/'([^']+)'/)[1]) : [];
83176
+ const resolved = total - missing.length;
83177
+ let severity;
83178
+ let message;
83179
+ if (resolved === total) {
83180
+ severity = "ok";
83181
+ message = `All ${total} default skills resolved`;
83182
+ } else if (resolved === 0) {
83183
+ severity = "error";
83184
+ message = `0/${total} default skills resolved \u2014 likely bundle stale. Run \`npm run build:bundle\`.`;
83185
+ } else {
83186
+ severity = "warn";
83187
+ message = `${resolved}/${total} default skills resolved \u2014 degraded: ${missing.join(", ")}`;
83188
+ }
83189
+ return { total, resolved, missing, severity, message };
83190
+ }
83191
+
82948
83192
  // src/extension/registration/command-registration.ts
82949
83193
  init_config();
82950
83194
  init_powerbar_publisher();
@@ -84551,7 +84795,6 @@ function startForegroundRunImpl(pi, ctx, extensionCtx, runner, runId) {
84551
84795
  init_config();
84552
84796
  import * as fs118 from "node:fs";
84553
84797
  import * as path94 from "node:path";
84554
- import { fileURLToPath as fileURLToPath9 } from "node:url";
84555
84798
 
84556
84799
  // src/runtime/per-write-validator.ts
84557
84800
  import { readFileSync as readFileSync92 } from "node:fs";
@@ -84622,6 +84865,7 @@ function buildValidationBlocker(filePath, error) {
84622
84865
  }
84623
84866
 
84624
84867
  // src/extension/registration/hook-registration.ts
84868
+ init_paths();
84625
84869
  init_safe_paths();
84626
84870
 
84627
84871
  // src/extension/team-tool/destructive-gate.ts
@@ -84643,18 +84887,177 @@ function shouldBlockDestructiveTeamAction(action, input) {
84643
84887
  return `Destructive action '${action}' requires confirm=true${action === "delete" ? " (or force=true to bypass reference checks)" : ""}.`;
84644
84888
  }
84645
84889
 
84890
+ // src/extension/registration/tool-loop-guard.ts
84891
+ var LOOP_GUARD_WARN_AT = 3;
84892
+ var LOOP_GUARD_BLOCK_AT = 5;
84893
+ var LOOP_GUARD_EXEMPT = {
84894
+ team: true,
84895
+ crew_agent: true,
84896
+ Agent: true,
84897
+ get_subagent_result: true
84898
+ };
84899
+ var LOOP_GUARD_BLOCK_TOOLS = {
84900
+ read: true,
84901
+ grep: true,
84902
+ glob: true,
84903
+ find: true,
84904
+ ls: true
84905
+ };
84906
+ var WAIT_TOOL = "ask";
84907
+ var WAIT_GUARD_WARN_AT = 2;
84908
+ var WAIT_GUARD_BLOCK_AT = 2;
84909
+ var MAX_TRACKED_FINGERPRINTS = 512;
84910
+ var LOOP_GUARD_MARKER = "[REPEATED TOOL CALLS - STOP]";
84911
+ var WAIT_GUARD_MARKER = "[REPEATED WAIT TOOL - END TURN]";
84912
+ var LOOP_GUARD_WARNING = `
84913
+ ${LOOP_GUARD_MARKER}
84914
+
84915
+ You have issued the exact same tool call with identical arguments ${LOOP_GUARD_WARN_AT} times in a row and received identical results. This is an infinite loop and you are making no progress.
84916
+
84917
+ STOP repeating this call. Instead:
84918
+ 1. Reconsider what you are looking for \u2014 the result above already contains what this call can tell you.
84919
+ 2. If you need different information, make a DIFFERENT call (different path, pattern, or tool).
84920
+ 3. If the task is actually done, produce your final answer now instead of calling more tools.
84921
+ `;
84922
+ var WAIT_GUARD_WARNING = `
84923
+ ${WAIT_GUARD_MARKER}
84924
+
84925
+ You have called \`ask\` ${WAIT_GUARD_WARN_AT} times in this turn. Its contract is to stop and wait \u2014 do not call it again in the same turn.
84926
+
84927
+ STOP calling tools that wait. Continue with what you can do, or produce your final answer; the reply arrives as a separate message later.
84928
+ `;
84929
+ function stableStringify(value) {
84930
+ return JSON.stringify(sortValue(value));
84931
+ }
84932
+ function sortValue(value) {
84933
+ if (Array.isArray(value)) return value.map(sortValue);
84934
+ if (value && typeof value === "object") {
84935
+ const record = value;
84936
+ const out = {};
84937
+ for (const key of Object.keys(record).sort()) {
84938
+ out[key] = sortValue(record[key]);
84939
+ }
84940
+ return out;
84941
+ }
84942
+ return value;
84943
+ }
84944
+ function fingerprint(tool, args) {
84945
+ return `${tool.toLowerCase()}:${stableStringify(args ?? null)}`;
84946
+ }
84947
+ function createLoopGuardState() {
84948
+ const runs = /* @__PURE__ */ new Map();
84949
+ let lastFingerprint = null;
84950
+ let waitRunCount = 0;
84951
+ function evictIfNeeded() {
84952
+ while (runs.size > MAX_TRACKED_FINGERPRINTS) {
84953
+ const oldest = runs.keys().next().value;
84954
+ if (oldest === void 0) break;
84955
+ runs.delete(oldest);
84956
+ }
84957
+ }
84958
+ function onToolResult(tool, args, output) {
84959
+ const toolLower = tool.toLowerCase();
84960
+ if (toolLower === WAIT_TOOL) {
84961
+ waitRunCount += 1;
84962
+ if (waitRunCount === WAIT_GUARD_WARN_AT) return [{ type: "text", text: WAIT_GUARD_WARNING }];
84963
+ return [];
84964
+ }
84965
+ waitRunCount = 0;
84966
+ if (LOOP_GUARD_EXEMPT[tool]) return [];
84967
+ const fp = fingerprint(tool, args);
84968
+ const outputKey = stableStringify(output);
84969
+ const state2 = runs.get(fp) ?? { runCount: 0, lastOutput: null };
84970
+ if (fp !== lastFingerprint) {
84971
+ state2.runCount = 0;
84972
+ }
84973
+ if (state2.lastOutput === outputKey) {
84974
+ state2.runCount += 1;
84975
+ } else {
84976
+ state2.runCount = 1;
84977
+ state2.lastOutput = outputKey;
84978
+ }
84979
+ runs.set(fp, state2);
84980
+ evictIfNeeded();
84981
+ lastFingerprint = fp;
84982
+ if (state2.runCount === LOOP_GUARD_WARN_AT) {
84983
+ return [{ type: "text", text: LOOP_GUARD_WARNING }];
84984
+ }
84985
+ return [];
84986
+ }
84987
+ function onToolCall(tool, args) {
84988
+ const toolLower = tool.toLowerCase();
84989
+ if (toolLower === WAIT_TOOL) {
84990
+ if (waitRunCount >= WAIT_GUARD_BLOCK_AT) {
84991
+ return {
84992
+ block: true,
84993
+ reason: `pi-crew loop guard: \`ask\` called ${waitRunCount} times this turn \u2014 its contract is to wait. End your turn; the reply arrives as a separate message.`
84994
+ };
84995
+ }
84996
+ return {};
84997
+ }
84998
+ if (LOOP_GUARD_EXEMPT[tool]) return {};
84999
+ if (!LOOP_GUARD_BLOCK_TOOLS[toolLower]) return {};
85000
+ const fp = fingerprint(tool, args);
85001
+ const state2 = runs.get(fp);
85002
+ if (state2 && state2.runCount >= LOOP_GUARD_BLOCK_AT) {
85003
+ return {
85004
+ block: true,
85005
+ reason: `pi-crew loop guard: this exact ${tool} call (identical arguments) has returned identical results ${state2.runCount} times in a row. Make a DIFFERENT call (different path, pattern, or tool), or produce your final answer.`
85006
+ };
85007
+ }
85008
+ return {};
85009
+ }
85010
+ function reset() {
85011
+ runs.clear();
85012
+ lastFingerprint = null;
85013
+ waitRunCount = 0;
85014
+ }
85015
+ return { onToolResult, onToolCall, reset };
85016
+ }
85017
+ function installToolLoopGuard(pi) {
85018
+ const state2 = createLoopGuardState();
85019
+ try {
85020
+ pi.on("tool_call", async (event) => {
85021
+ const verdict = state2.onToolCall(event.toolName, event.input);
85022
+ if (verdict.block) {
85023
+ return { block: true, reason: verdict.reason };
85024
+ }
85025
+ return void 0;
85026
+ });
85027
+ } catch {
85028
+ }
85029
+ try {
85030
+ pi.on("tool_result", (event) => {
85031
+ const appends = state2.onToolResult(event.toolName, event.input, event.content);
85032
+ if (appends.length === 0) return void 0;
85033
+ const existing = Array.isArray(event.content) ? event.content : [];
85034
+ return { content: [...existing, ...appends] };
85035
+ });
85036
+ } catch {
85037
+ }
85038
+ }
85039
+
84646
85040
  // src/extension/registration/hook-registration.ts
84647
85041
  function installPiHooks(pi, ctx) {
84648
85042
  installResourcesDiscoverHook(pi, ctx);
84649
85043
  installToolCallHook(pi, ctx);
84650
85044
  installToolResultHook(pi, ctx);
85045
+ installToolLoopGuardIfEnabled(pi, ctx);
85046
+ }
85047
+ function installToolLoopGuardIfEnabled(pi, ctx) {
85048
+ try {
85049
+ const cwd = ctx.currentCtx?.cwd ?? process.cwd();
85050
+ if (loadConfig(cwd).config.reliability?.loopGuard === false) return;
85051
+ } catch {
85052
+ }
85053
+ installToolLoopGuard(pi);
84651
85054
  }
84652
85055
  function installResourcesDiscoverHook(pi, ctx) {
84653
85056
  try {
84654
85057
  pi.on("resources_discover", () => {
84655
85058
  const sessionCwd = ctx.currentCtx?.cwd ?? process.cwd();
84656
85059
  const skillDir = path94.resolve(sessionCwd, "skills");
84657
- const extSkillDir = path94.resolve(path94.dirname(fileURLToPath9(import.meta.url)), "..", "..", "skills");
85060
+ const extSkillDir = path94.join(packageRoot(), "skills");
84658
85061
  const paths = [];
84659
85062
  if (fs118.existsSync(extSkillDir)) paths.push(extSkillDir);
84660
85063
  if (skillDir !== extSkillDir && fs118.existsSync(skillDir)) {
@@ -85244,7 +85647,7 @@ function safeStringify(value) {
85244
85647
  }
85245
85648
  }
85246
85649
  var WAIT_REQUEST_TIMEOUT_SEC_MAX = 3600;
85247
- var WAIT_REQUEST_TIMEOUT_SEC_DEFAULT = 600;
85650
+ var WAIT_REQUEST_TIMEOUT_SEC_DEFAULT = 480;
85248
85651
  var WAIT_QUESTION_MAX_CHARS = 8192;
85249
85652
  var WAIT_OPTIONS_MAX = 16;
85250
85653
  var WAIT_OPTION_MAX_CHARS = 256;
@@ -85352,6 +85755,29 @@ function recordWaitPolicyRejection(manifest, taskId, method) {
85352
85755
  );
85353
85756
  }
85354
85757
 
85758
+ // src/runtime/broker/wait-push.ts
85759
+ init_state_store();
85760
+ async function pushWaitingToForegroundWaiter(params) {
85761
+ try {
85762
+ const { resolveRunPromise: resolveRunPromise2 } = await Promise.resolve().then(() => (init_run_tracker(), run_tracker_exports));
85763
+ const freshPark = loadRunManifestById(params.cwd ?? process.cwd(), params.runId);
85764
+ if (freshPark) {
85765
+ resolveRunPromise2(params.runId, {
85766
+ manifest: freshPark.manifest,
85767
+ tasks: freshPark.tasks,
85768
+ waiting: {
85769
+ taskId: params.taskId,
85770
+ questionId: params.questionId,
85771
+ question: params.question,
85772
+ deadline: params.deadline,
85773
+ ...params.options ? { options: params.options } : {}
85774
+ }
85775
+ });
85776
+ }
85777
+ } catch {
85778
+ }
85779
+ }
85780
+
85355
85781
  // src/runtime/broker/wait-status-cache.ts
85356
85782
  init_state_store();
85357
85783
  import * as fs123 from "node:fs";
@@ -86309,22 +86735,13 @@ var CrewBroker = class {
86309
86735
  });
86310
86736
  await pollUntilDone();
86311
86737
  }
86312
- /**
86313
- * Phase 3: steer.push — push steering message to a running worker.
86314
- *
86315
- * Dual-write strategy for durability:
86316
- * 1. Mailbox append (appendMailboxMessageAsync) — feeds the live broker
86317
- * fanout to connected subscribers AND persists to the mailbox inbox
86318
- * JSONL for later read.
86319
- * 2. Steering-file append — writes the steer body to
86320
- * ${artifactsRoot}/steering/${taskId}.jsonl, the same file the
86321
- * child's pollSteering() polls via PI_CREW_STEERING_FILE. This is
86322
- * the durable fallback: even if the recipient child's broker connection is down, the
86323
- * child picks up the steer on its next poll tick.
86324
- *
86325
- * A steering-file write failure does NOT fail the steer push — the
86326
- * mailbox write (1) has already succeeded.
86327
- */
86738
+ /** Phase 3: steer.push — push steering message to a running worker.
86739
+ * Dual-write for durability: (1) mailbox append feeds the live broker
86740
+ * fanout AND persists to the inbox JSONL; (2) steering-file append writes
86741
+ * ${artifactsRoot}/steering/${taskId}.jsonl — the durable fallback the
86742
+ * child's pollSteering() polls via PI_CREW_STEERING_FILE even when its
86743
+ * broker connection is down. A steering-file write failure does NOT fail
86744
+ * the push — the mailbox write has already succeeded. */
86328
86745
  async handleSteerPush(conn, id, params) {
86329
86746
  if (conn.role !== "orchestrator") {
86330
86747
  this.sendError(conn, id, "forbidden", "steer.push requires orchestrator role");
@@ -86879,6 +87296,15 @@ ${sanitizedText}
86879
87296
  timeoutSec: clampSec,
86880
87297
  clamped
86881
87298
  });
87299
+ await pushWaitingToForegroundWaiter({
87300
+ cwd: this.options.cwd,
87301
+ runId,
87302
+ taskId,
87303
+ questionId,
87304
+ question: parsed.question,
87305
+ deadline,
87306
+ ...parsed.options ? { options: parsed.options } : {}
87307
+ });
86882
87308
  }
86883
87309
  /** WP-2/R2: terminal report of the parked `ask` tool — flips the task
86884
87310
  * waiting→running and clears the park coordination state. Scoped to
@@ -86989,7 +87415,9 @@ init_child_pi();
86989
87415
 
86990
87416
  // src/runtime/detached-run-results.ts
86991
87417
  init_state_store();
87418
+ init_internal_error();
86992
87419
  init_process_status();
87420
+ var MAX_DELIVERY_ATTEMPTS = 3;
86993
87421
  var detachedRuns = /* @__PURE__ */ new Map();
86994
87422
  function hasDetachedRuns() {
86995
87423
  return detachedRuns.size > 0;
@@ -87025,6 +87453,17 @@ function peekFinishedDetachedRunResults(options = {}) {
87025
87453
  continue;
87026
87454
  }
87027
87455
  if (!isFinishedRunStatus(loaded.manifest.status)) continue;
87456
+ entry.attempts += 1;
87457
+ if (entry.attempts > MAX_DELIVERY_ATTEMPTS) {
87458
+ detachedRuns.delete(entry.runId);
87459
+ logInternalError(
87460
+ "detached-run-results.delivery-gave-up",
87461
+ new Error("delivery attempts exceeded"),
87462
+ `runId=${entry.runId} attempts=${entry.attempts} \u2014 dropped after ${MAX_DELIVERY_ATTEMPTS} failed sends`,
87463
+ "warn"
87464
+ );
87465
+ continue;
87466
+ }
87028
87467
  ready.push({ runId: entry.runId, text: formatDetachedRunResult(loaded.manifest, loaded.tasks) });
87029
87468
  }
87030
87469
  return ready;
@@ -87500,10 +87939,10 @@ var CrewAgentPane = class {
87500
87939
  this.lastTranscriptReadAt = Date.now();
87501
87940
  }
87502
87941
  const items = this.lastItems;
87503
- const fingerprint = this.bodyFingerprint(items, width);
87504
- if (fingerprint !== this.bodyKey) {
87942
+ const fingerprint2 = this.bodyFingerprint(items, width);
87943
+ if (fingerprint2 !== this.bodyKey) {
87505
87944
  this.cachedBody = this.buildBody(items, width);
87506
- this.bodyKey = fingerprint;
87945
+ this.bodyKey = fingerprint2;
87507
87946
  }
87508
87947
  const body = this.cachedBody;
87509
87948
  const header = this.headerLines(manifest, width);
@@ -90299,7 +90738,7 @@ function installCrossExtensionWiring(pi, ctx) {
90299
90738
  }
90300
90739
 
90301
90740
  // src/extension/register.ts
90302
- function registerPiTeams(pi) {
90741
+ async function registerPiTeams(pi) {
90303
90742
  resetTimings();
90304
90743
  time("register:start");
90305
90744
  installChildProcessAbortShield();
@@ -90349,6 +90788,12 @@ function registerPiTeams(pi) {
90349
90788
  } catch (err2) {
90350
90789
  console.warn("[pi-crew] crew-vibes initialization failed:", err2 instanceof Error ? err2.message : err2);
90351
90790
  }
90791
+ const skillCheck = await runPostInitSkillCheck(process.cwd());
90792
+ if (skillCheck.severity === "error") {
90793
+ console.error(`[pi-crew] ${skillCheck.message}`);
90794
+ } else if (skillCheck.severity === "warn") {
90795
+ console.warn(`[pi-crew] ${skillCheck.message}`);
90796
+ }
90352
90797
  }
90353
90798
 
90354
90799
  // index.bundle.ts
@@ -90359,6 +90804,7 @@ function index_bundle_default(pi) {
90359
90804
  export {
90360
90805
  index_bundle_default as default,
90361
90806
  registerPiTeams,
90807
+ runPostInitSkillCheck,
90362
90808
  waitForRun
90363
90809
  };
90364
90810
  //# sourceMappingURL=index.mjs.map