pi-crew 0.9.64 → 0.9.65

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 (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +46 -1
  3. package/dist/index.mjs +316 -299
  4. package/package.json +3 -2
  5. package/scripts/analyze-run.mjs +1333 -0
  6. package/scripts/pty_probe.py +10 -8
  7. package/scripts/resource-sampler.mjs +482 -0
  8. package/skills/real-test-pi-crew/SKILL.md +6 -6
  9. package/src/observability/event-to-metric.ts +29 -0
  10. package/src/observability/metrics-primitives.ts +41 -3
  11. package/src/runtime/README.md +1 -1
  12. package/src/runtime/broker/crew-broker.ts +0 -16
  13. package/src/runtime/effectiveness.ts +23 -1
  14. package/src/runtime/merge-gate.ts +202 -0
  15. package/src/runtime/model/model-fallback.ts +11 -0
  16. package/src/runtime/model/provider-extensions.ts +31 -12
  17. package/src/runtime/output/progress-tracker.ts +3 -33
  18. package/src/runtime/scratchpad/engine.ts +40 -2
  19. package/src/runtime/scratchpad/snapshot-hmac.ts +161 -0
  20. package/src/runtime/team-runner.ts +128 -203
  21. package/src/schema/team-tool-schema.ts +2 -0
  22. package/src/teams/discover-teams.ts +2 -0
  23. package/src/teams/team-config.ts +7 -0
  24. package/src/teams/team-serializer.ts +1 -0
  25. package/src/ui/mascot.ts +1 -14
  26. package/teams/default.team.md +1 -0
  27. package/teams/fast-fix.team.md +1 -0
  28. package/src/observability/event-bus.ts +0 -86
  29. package/src/plugins/plugin-define.ts +0 -6
  30. package/src/plugins/plugin-registry.ts +0 -32
  31. package/src/plugins/plugins/index.ts +0 -3
  32. package/src/plugins/plugins/nextjs.ts +0 -19
  33. package/src/plugins/plugins/vite.ts +0 -10
  34. package/src/plugins/plugins/vitest.ts +0 -9
  35. package/src/runtime/child-pi/child-pi-pool.ts +0 -68
  36. package/src/runtime/iteration-hooks.ts +0 -305
package/dist/index.mjs CHANGED
@@ -11513,10 +11513,16 @@ function discoverProviderExtensions(settingsPath2) {
11513
11513
  const npmBase = path9.join(baseDir, "npm", "node_modules");
11514
11514
  for (const spec of settings.packages ?? []) {
11515
11515
  if (typeof spec !== "string") continue;
11516
- if (!spec.startsWith("npm:")) continue;
11517
- const pkgName = spec.slice(4);
11518
- const pkgDir = path9.join(npmBase, pkgName);
11516
+ let pkgDir;
11517
+ if (spec.startsWith("npm:")) {
11518
+ pkgDir = path9.join(npmBase, spec.slice(4));
11519
+ } else if (spec.startsWith("./") || spec.startsWith("../") || path9.isAbsolute(spec)) {
11520
+ pkgDir = path9.resolve(baseDir, spec);
11521
+ } else {
11522
+ continue;
11523
+ }
11519
11524
  if (!fs9.existsSync(pkgDir)) continue;
11525
+ if (path9.resolve(pkgDir) === path9.resolve(packageRoot())) continue;
11520
11526
  const entryPath = resolvePackageEntry(pkgDir);
11521
11527
  if (entryPath) out.push({ spec, entryPath });
11522
11528
  }
@@ -12442,6 +12448,8 @@ function parseTeamFile(filePath, source) {
12442
12448
  defaultWorkflow: frontmatter.defaultWorkflow || frontmatter.workflow || void 0,
12443
12449
  workspaceMode: frontmatter.workspaceMode?.trim() === "worktree" ? "worktree" : "single",
12444
12450
  maxConcurrency: frontmatter.maxConcurrency ? Number.parseInt(frontmatter.maxConcurrency, 10) : void 0,
12451
+ // observability defaults ON ("luôn hoạt động"); explicit `observability: false` disables.
12452
+ observability: frontmatter.observability === void 0 ? true : frontmatter.observability !== "false",
12445
12453
  routing: triggers || useWhen || avoidWhen || cost || category ? { triggers, useWhen, avoidWhen, cost, category } : void 0
12446
12454
  };
12447
12455
  } catch {
@@ -22404,7 +22412,18 @@ var init_model_fallback = __esm({
22404
22412
  /context[_ ]?length[_ ]?exceeded/i,
22405
22413
  /safety/i,
22406
22414
  /is[_ ]?overloaded/i,
22407
- /\b408\b/
22415
+ /\b408\b/,
22416
+ //
22417
+ // EPIPE / broken-pipe. In the child-pi worker path this typically means
22418
+ // the child `pi` process exited (crash or early exit) while the parent
22419
+ // was still writing to its stdin — spawning a fresh child on the next
22420
+ // model in the fallback chain usually recovers. In the network path it
22421
+ // is a transient pipe close. Both are retryable on a different model.
22422
+ // See docs/failure-mode-inventory.md EPIPE gap; NON_RETRYABLE patterns
22423
+ // (auth/billing) are checked first, so an auth error mentioning EPIPE
22424
+ // stays non-retryable.
22425
+ /epipe/i,
22426
+ /broken pipe/i
22408
22427
  ];
22409
22428
  NON_RETRYABLE_MODEL_FAILURE_PATTERNS = [
22410
22429
  /auth(?:entication)?/i,
@@ -23842,10 +23861,12 @@ var init_team_tool_schema = __esm({
23842
23861
  })
23843
23862
  ),
23844
23863
  budgetTotal: Type.Optional(
23864
+ // Empty-string unset marker accepted (Tier-9: models emit "" when unset).
23845
23865
  // 0 accepted as "unset/disabled" (models emit 0 for off); still rejects 1-999
23846
23866
  // as the MISCONFIGURATION GUARD against typo'd silent-abort configs.
23847
23867
  Type.Union(
23848
23868
  [
23869
+ Type.Literal(""),
23849
23870
  Type.Literal(0),
23850
23871
  Type.Number({
23851
23872
  minimum: 1e3
@@ -39565,6 +39586,7 @@ function serializeTeam(team) {
39565
39586
  team.defaultWorkflow ? `defaultWorkflow: ${team.defaultWorkflow}` : void 0,
39566
39587
  team.workspaceMode ? `workspaceMode: ${team.workspaceMode}` : void 0,
39567
39588
  team.maxConcurrency !== void 0 ? `maxConcurrency: ${team.maxConcurrency}` : void 0,
39589
+ team.observability !== void 0 ? `observability: ${team.observability}` : void 0,
39568
39590
  line2("triggers", team.routing?.triggers),
39569
39591
  line2("useWhen", team.routing?.useWhen),
39570
39592
  line2("avoidWhen", team.routing?.avoidWhen),
@@ -48455,6 +48477,9 @@ function taskHasObservableWorkerActivity(task) {
48455
48477
  (task.agentProgress?.toolCount ?? 0) > 0 || task.usage || task.transcriptArtifact || task.modelAttempts?.some((attempt) => attempt.success) || task.jsonEvents
48456
48478
  );
48457
48479
  }
48480
+ function taskHasEmptyResult(task) {
48481
+ return Boolean(task.resultArtifact && task.resultArtifact.sizeBytes === 0);
48482
+ }
48458
48483
  function resolveEffectivenessGuardMode(runtimeConfig, manifest) {
48459
48484
  const configured = runtimeConfig?.effectivenessGuard;
48460
48485
  if (configured === "off" || configured === "warn" || configured === "block" || configured === "fail") return configured;
@@ -48463,7 +48488,7 @@ function resolveEffectivenessGuardMode(runtimeConfig, manifest) {
48463
48488
  }
48464
48489
  function evaluateRunEffectiveness(input) {
48465
48490
  const completedTasks = input.tasks.filter((task) => task.status === "completed");
48466
- const noObservedWorkTasks = completedTasks.filter((task) => !taskHasObservableWorkerActivity(task));
48491
+ const noObservedWorkTasks = completedTasks.filter((task) => !taskHasObservableWorkerActivity(task) || taskHasEmptyResult(task));
48467
48492
  const needsAttentionTasks = input.tasks.filter((task) => task.agentProgress?.activityState === "needs_attention");
48468
48493
  const workerExecution = input.executeWorkers ? "enabled" : "disabled/scaffold";
48469
48494
  const guardMode = resolveEffectivenessGuardMode(input.runtimeConfig, input.manifest);
@@ -49325,112 +49350,6 @@ var init_correlation = __esm({
49325
49350
  }
49326
49351
  });
49327
49352
 
49328
- // src/plugins/plugin-registry.ts
49329
- var PluginRegistry;
49330
- var init_plugin_registry = __esm({
49331
- "src/plugins/plugin-registry.ts"() {
49332
- "use strict";
49333
- PluginRegistry = class {
49334
- plugins = [];
49335
- register(plugin) {
49336
- this.plugins.push(plugin);
49337
- }
49338
- activePlugins(allDeps) {
49339
- return this.plugins.filter(
49340
- (p) => p.enablers.some((enabler) => {
49341
- if (enabler.endsWith("/")) {
49342
- return allDeps.some((d) => d.startsWith(enabler));
49343
- }
49344
- return allDeps.includes(enabler);
49345
- })
49346
- );
49347
- }
49348
- allPlugins() {
49349
- return [...this.plugins];
49350
- }
49351
- };
49352
- }
49353
- });
49354
-
49355
- // src/plugins/plugin-define.ts
49356
- function definePlugin(spec) {
49357
- return spec;
49358
- }
49359
- var init_plugin_define = __esm({
49360
- "src/plugins/plugin-define.ts"() {
49361
- "use strict";
49362
- }
49363
- });
49364
-
49365
- // src/plugins/plugins/nextjs.ts
49366
- var NextJsPlugin;
49367
- var init_nextjs = __esm({
49368
- "src/plugins/plugins/nextjs.ts"() {
49369
- "use strict";
49370
- init_plugin_define();
49371
- NextJsPlugin = definePlugin({
49372
- name: "nextjs",
49373
- enablers: ["next"],
49374
- entryPatterns: [
49375
- "src/app/**/*.{ts,tsx}",
49376
- "src/pages/**/*.{ts,tsx}",
49377
- "src/app/**/page.{ts,tsx}",
49378
- "src/app/**/layout.{ts,tsx}",
49379
- "src/app/**/route.{ts,tsx}",
49380
- "middleware.{ts,js}",
49381
- "next.config.{ts,js,mjs}"
49382
- ],
49383
- configPatterns: ["next.config.{ts,js,mjs}"],
49384
- toolingDependencies: ["next", "@next/font", "@next/mdx"],
49385
- pathAliases: [["~", "src"]],
49386
- virtualModulePrefixes: ["next:"]
49387
- });
49388
- }
49389
- });
49390
-
49391
- // src/plugins/plugins/vite.ts
49392
- var VitePlugin;
49393
- var init_vite = __esm({
49394
- "src/plugins/plugins/vite.ts"() {
49395
- "use strict";
49396
- init_plugin_define();
49397
- VitePlugin = definePlugin({
49398
- name: "vite",
49399
- enablers: ["vite", "rolldown-vite"],
49400
- entryPatterns: ["src/main.{ts,tsx,js,jsx}", "src/index.{ts,tsx,js,jsx}", "index.html"],
49401
- configPatterns: ["vite.config.{ts,js,mts,mjs}"],
49402
- toolingDependencies: ["vite"],
49403
- virtualModulePrefixes: ["virtual:"]
49404
- });
49405
- }
49406
- });
49407
-
49408
- // src/plugins/plugins/vitest.ts
49409
- var VitestPlugin;
49410
- var init_vitest = __esm({
49411
- "src/plugins/plugins/vitest.ts"() {
49412
- "use strict";
49413
- init_plugin_define();
49414
- VitestPlugin = definePlugin({
49415
- name: "vitest",
49416
- enablers: ["vitest"],
49417
- entryPatterns: ["**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}", "src/**/*.test.{ts,tsx}", "src/**/*.spec.{ts,tsx}"],
49418
- configPatterns: ["vitest.config.{ts,js,mjs}", "vite.config.ts"],
49419
- toolingDependencies: ["vitest"]
49420
- });
49421
- }
49422
- });
49423
-
49424
- // src/plugins/plugins/index.ts
49425
- var init_plugins = __esm({
49426
- "src/plugins/plugins/index.ts"() {
49427
- "use strict";
49428
- init_nextjs();
49429
- init_vite();
49430
- init_vitest();
49431
- }
49432
- });
49433
-
49434
49353
  // src/runtime/task-health.ts
49435
49354
  function scoreToGrade(score) {
49436
49355
  if (score >= 90) return "A";
@@ -50470,6 +50389,166 @@ var init_group_join = __esm({
50470
50389
  }
50471
50390
  });
50472
50391
 
50392
+ // src/runtime/scheduling/task-graph-scheduler.ts
50393
+ function buildTaskGraphIndex(tasks) {
50394
+ const cached2 = taskGraphIndexCache.get(tasks);
50395
+ if (cached2) return cached2;
50396
+ const fresh = {
50397
+ doneSteps: new Set(
50398
+ tasks.filter((task) => task.status === "completed").map((task) => task.stepId).filter((id) => id !== void 0)
50399
+ ),
50400
+ idMap: new Map(tasks.map((task) => [task.id, task])),
50401
+ stepToTaskId: new Map(
50402
+ tasks.map((task) => [task.stepId, task.id]).filter((entry) => entry[0] !== void 0)
50403
+ )
50404
+ };
50405
+ taskGraphIndexCache.set(tasks, fresh);
50406
+ return fresh;
50407
+ }
50408
+ function dependencySatisfied(task, doneStepIds, idMap, stepMap) {
50409
+ return task.dependsOn.every((dependency) => {
50410
+ if (doneStepIds.has(dependency)) return true;
50411
+ const taskId = stepMap.get(dependency) ?? dependency;
50412
+ return idMap.get(taskId)?.status === "completed";
50413
+ });
50414
+ }
50415
+ function withQueue(task, index) {
50416
+ let resolvedQueue;
50417
+ if (task.status === "queued") {
50418
+ const isReady = dependencySatisfied(task, index.doneSteps, index.idMap, index.stepToTaskId);
50419
+ resolvedQueue = isReady ? "ready" : "blocked";
50420
+ } else if (task.status === "running") {
50421
+ resolvedQueue = "running";
50422
+ } else if (task.status === "completed" || task.status === "skipped" || task.status === "needs_attention") {
50423
+ resolvedQueue = "done";
50424
+ } else {
50425
+ resolvedQueue = "blocked";
50426
+ }
50427
+ if (task.graph && task.graph.queue === resolvedQueue) {
50428
+ return task;
50429
+ }
50430
+ return {
50431
+ ...task,
50432
+ graph: task.graph ? { ...task.graph, queue: resolvedQueue } : task.graph
50433
+ };
50434
+ }
50435
+ function ensureIndex(tasks, index) {
50436
+ return index ?? buildTaskGraphIndex(tasks);
50437
+ }
50438
+ function refreshTaskGraphQueues(tasks, index) {
50439
+ const resolved = ensureIndex(tasks, index);
50440
+ return tasks.map((task) => withQueue(task, resolved));
50441
+ }
50442
+ function taskGraphSnapshot(tasks, index) {
50443
+ const refreshed = refreshTaskGraphQueues(tasks, index);
50444
+ return {
50445
+ ready: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "ready").map((task) => task.id),
50446
+ blocked: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "blocked").map((task) => task.id),
50447
+ running: refreshed.filter((task) => task.status === "running").map((task) => task.id),
50448
+ done: refreshed.filter((task) => task.status === "completed" || task.status === "skipped").map((task) => task.id),
50449
+ failed: refreshed.filter((task) => task.status === "failed").map((task) => task.id),
50450
+ cancelled: refreshed.filter((task) => task.status === "cancelled").map((task) => task.id)
50451
+ };
50452
+ }
50453
+ var taskGraphIndexCache;
50454
+ var init_task_graph_scheduler = __esm({
50455
+ "src/runtime/scheduling/task-graph-scheduler.ts"() {
50456
+ "use strict";
50457
+ taskGraphIndexCache = /* @__PURE__ */ new WeakMap();
50458
+ }
50459
+ });
50460
+
50461
+ // src/runtime/merge-gate.ts
50462
+ function isNonTerminalTaskStatus(status) {
50463
+ return status === "queued" || status === "running" || status === "waiting";
50464
+ }
50465
+ function safeFinishedAt(task) {
50466
+ if (!task.finishedAt) return -Infinity;
50467
+ const ms = new Date(task.finishedAt).getTime();
50468
+ return Number.isNaN(ms) ? Infinity : ms;
50469
+ }
50470
+ function isMalformedFinishedAtReplacement(currentTime, updatedTime) {
50471
+ return !Number.isFinite(currentTime) && Number.isFinite(updatedTime);
50472
+ }
50473
+ function statusMergeKey(from, to) {
50474
+ return `${from}->${to}`;
50475
+ }
50476
+ function shouldMergeTaskUpdate(current, updated) {
50477
+ if (REJECTED_STATUS_MERGE_TRANSITIONS.has(statusMergeKey(current.status, updated.status))) return false;
50478
+ if (current.status === updated.status && updated.status === "running" && current.resultArtifact && !updated.resultArtifact)
50479
+ return false;
50480
+ if (current.status === updated.status && current.status === "completed" && current.resultArtifact && !updated.resultArtifact)
50481
+ return false;
50482
+ if (current.finishedAt !== void 0 && updated.finishedAt !== void 0) {
50483
+ const currentTime = safeFinishedAt(current);
50484
+ const updatedTime = safeFinishedAt(updated);
50485
+ if (!Number.isFinite(currentTime)) {
50486
+ console.warn(`[merge-gate] Task ${current.id} has malformed finishedAt: ${current.finishedAt}`);
50487
+ }
50488
+ if (isMalformedFinishedAtReplacement(currentTime, updatedTime)) {
50489
+ return true;
50490
+ }
50491
+ if (updatedTime < currentTime) return false;
50492
+ }
50493
+ if (!updated.finishedAt && !isNonTerminalTaskStatus(updated.status)) return false;
50494
+ const hasMeaningfulUpdate = updated.status !== current.status || updated.finishedAt !== current.finishedAt || updated.startedAt !== current.startedAt || Boolean(updated.resultArtifact) !== Boolean(current.resultArtifact) || Boolean(updated.resultArtifact) && updated.resultArtifact !== current.resultArtifact || Boolean(updated.error) || Boolean(updated.modelAttempts?.length) || Boolean(updated.usage) || Boolean(updated.attempts?.length) || updated.heartbeat?.lastSeenAt !== current.heartbeat?.lastSeenAt || updated.jsonEvents !== current.jsonEvents || updated.agentProgress?.lastActivityAt !== current.agentProgress?.lastActivityAt;
50495
+ return hasMeaningfulUpdate;
50496
+ }
50497
+ function mergeTaskUpdatesPreservingTerminal(base, results) {
50498
+ const indexById = /* @__PURE__ */ new Map();
50499
+ for (const task of base) indexById.set(task.id, task);
50500
+ let skipped = 0;
50501
+ for (const result4 of results) {
50502
+ for (const updated of result4.tasks) {
50503
+ const current = indexById.get(updated.id);
50504
+ if (!current) continue;
50505
+ if (!shouldMergeTaskUpdate(current, updated)) {
50506
+ console.debug("[merge-gate] Skipping stale merge for task", updated.id, {
50507
+ currentStatus: current.status,
50508
+ updatedStatus: updated.status,
50509
+ currentFinishedAt: current.finishedAt,
50510
+ updatedFinishedAt: updated.finishedAt
50511
+ });
50512
+ skipped += 1;
50513
+ continue;
50514
+ }
50515
+ indexById.set(updated.id, updated);
50516
+ }
50517
+ }
50518
+ const merged = base.map((task) => indexById.get(task.id) ?? task);
50519
+ void skipped;
50520
+ return refreshTaskGraphQueues(merged);
50521
+ }
50522
+ var REJECTED_STATUS_MERGE_TRANSITIONS, __test__shouldMergeTaskUpdate, __test__mergeTaskUpdates;
50523
+ var init_merge_gate = __esm({
50524
+ "src/runtime/merge-gate.ts"() {
50525
+ "use strict";
50526
+ init_contracts();
50527
+ init_task_graph_scheduler();
50528
+ REJECTED_STATUS_MERGE_TRANSITIONS = (() => {
50529
+ const rejected = /* @__PURE__ */ new Set();
50530
+ for (const from of TEAM_TASK_STATUSES) {
50531
+ if (!TEAM_TERMINAL_TASK_STATUSES.has(from)) continue;
50532
+ for (const to of TEAM_TASK_STATUSES) {
50533
+ if (!TEAM_TERMINAL_TASK_STATUSES.has(to)) rejected.add(statusMergeKey(from, to));
50534
+ }
50535
+ }
50536
+ rejected.add(statusMergeKey("waiting", "running"));
50537
+ const completedIntegrityFlips = [
50538
+ ["completed", "failed"],
50539
+ ["completed", "needs_attention"],
50540
+ ["failed", "completed"],
50541
+ ["cancelled", "completed"],
50542
+ ["needs_attention", "completed"]
50543
+ ];
50544
+ for (const [from, to] of completedIntegrityFlips) rejected.add(statusMergeKey(from, to));
50545
+ return rejected;
50546
+ })();
50547
+ __test__shouldMergeTaskUpdate = shouldMergeTaskUpdate;
50548
+ __test__mergeTaskUpdates = mergeTaskUpdatesPreservingTerminal;
50549
+ }
50550
+ });
50551
+
50473
50552
  // src/runtime/model/runtime-policy.ts
50474
50553
  function resolveTaskRuntimeKind(globalKind, role, isolationPolicy, env = process.env) {
50475
50554
  if (globalKind === "scaffold") return "scaffold";
@@ -51822,75 +51901,6 @@ var init_task_graph = __esm({
51822
51901
  }
51823
51902
  });
51824
51903
 
51825
- // src/runtime/scheduling/task-graph-scheduler.ts
51826
- function buildTaskGraphIndex(tasks) {
51827
- const cached2 = taskGraphIndexCache.get(tasks);
51828
- if (cached2) return cached2;
51829
- const fresh = {
51830
- doneSteps: new Set(
51831
- tasks.filter((task) => task.status === "completed").map((task) => task.stepId).filter((id) => id !== void 0)
51832
- ),
51833
- idMap: new Map(tasks.map((task) => [task.id, task])),
51834
- stepToTaskId: new Map(
51835
- tasks.map((task) => [task.stepId, task.id]).filter((entry) => entry[0] !== void 0)
51836
- )
51837
- };
51838
- taskGraphIndexCache.set(tasks, fresh);
51839
- return fresh;
51840
- }
51841
- function dependencySatisfied(task, doneStepIds, idMap, stepMap) {
51842
- return task.dependsOn.every((dependency) => {
51843
- if (doneStepIds.has(dependency)) return true;
51844
- const taskId = stepMap.get(dependency) ?? dependency;
51845
- return idMap.get(taskId)?.status === "completed";
51846
- });
51847
- }
51848
- function withQueue(task, index) {
51849
- let resolvedQueue;
51850
- if (task.status === "queued") {
51851
- const isReady = dependencySatisfied(task, index.doneSteps, index.idMap, index.stepToTaskId);
51852
- resolvedQueue = isReady ? "ready" : "blocked";
51853
- } else if (task.status === "running") {
51854
- resolvedQueue = "running";
51855
- } else if (task.status === "completed" || task.status === "skipped" || task.status === "needs_attention") {
51856
- resolvedQueue = "done";
51857
- } else {
51858
- resolvedQueue = "blocked";
51859
- }
51860
- if (task.graph && task.graph.queue === resolvedQueue) {
51861
- return task;
51862
- }
51863
- return {
51864
- ...task,
51865
- graph: task.graph ? { ...task.graph, queue: resolvedQueue } : task.graph
51866
- };
51867
- }
51868
- function ensureIndex(tasks, index) {
51869
- return index ?? buildTaskGraphIndex(tasks);
51870
- }
51871
- function refreshTaskGraphQueues(tasks, index) {
51872
- const resolved = ensureIndex(tasks, index);
51873
- return tasks.map((task) => withQueue(task, resolved));
51874
- }
51875
- function taskGraphSnapshot(tasks, index) {
51876
- const refreshed = refreshTaskGraphQueues(tasks, index);
51877
- return {
51878
- ready: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "ready").map((task) => task.id),
51879
- blocked: refreshed.filter((task) => task.status === "queued" && task.graph?.queue === "blocked").map((task) => task.id),
51880
- running: refreshed.filter((task) => task.status === "running").map((task) => task.id),
51881
- done: refreshed.filter((task) => task.status === "completed" || task.status === "skipped").map((task) => task.id),
51882
- failed: refreshed.filter((task) => task.status === "failed").map((task) => task.id),
51883
- cancelled: refreshed.filter((task) => task.status === "cancelled").map((task) => task.id)
51884
- };
51885
- }
51886
- var taskGraphIndexCache;
51887
- var init_task_graph_scheduler = __esm({
51888
- "src/runtime/scheduling/task-graph-scheduler.ts"() {
51889
- "use strict";
51890
- taskGraphIndexCache = /* @__PURE__ */ new WeakMap();
51891
- }
51892
- });
51893
-
51894
51904
  // src/extension/knowledge-injection.ts
51895
51905
  import * as fs75 from "node:fs";
51896
51906
  import * as path65 from "node:path";
@@ -56787,12 +56797,13 @@ __export(team_runner_exports, {
56787
56797
  drainPendingUnits: () => drainPendingUnits,
56788
56798
  executeTeamRun: () => executeTeamRun,
56789
56799
  hasPendingMutatingTaskAtBoundary: () => hasPendingMutatingTaskAtBoundary,
56790
- mergeTaskUpdatesPreservingTerminal: () => mergeTaskUpdatesPreservingTerminal,
56791
56800
  setRunStatusRunning: () => setRunStatusRunning,
56792
56801
  shouldUseRetry: () => shouldUseRetry
56793
56802
  });
56803
+ import { spawn as spawn6 } from "node:child_process";
56794
56804
  import * as fs86 from "node:fs";
56795
56805
  import * as path72 from "node:path";
56806
+ import { fileURLToPath as fileURLToPath7 } from "node:url";
56796
56807
  function startTeamRunHeartbeat(stateRoot, runId) {
56797
56808
  const heartbeatPath = path72.join(stateRoot, "heartbeat.json");
56798
56809
  const writeHeartbeat = () => {
@@ -56816,6 +56827,87 @@ function startTeamRunHeartbeat(stateRoot, runId) {
56816
56827
  const interval = setInterval(writeHeartbeat, 6e4);
56817
56828
  return () => clearInterval(interval);
56818
56829
  }
56830
+ function perfScriptPath(scriptName) {
56831
+ try {
56832
+ const candidates = [
56833
+ fileURLToPath7(new URL(`../../scripts/${scriptName}`, import.meta.url)),
56834
+ fileURLToPath7(new URL(`../scripts/${scriptName}`, import.meta.url))
56835
+ ];
56836
+ return candidates.find((p) => fs86.existsSync(p));
56837
+ } catch {
56838
+ return void 0;
56839
+ }
56840
+ }
56841
+ function startPerfSampler(manifest, team) {
56842
+ const marker = (msg) => {
56843
+ try {
56844
+ fs86.appendFileSync(path72.join(manifest.artifactsRoot, "perf-obs.log"), `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
56845
+ `);
56846
+ } catch {
56847
+ }
56848
+ };
56849
+ marker(`startPerfSampler entered (team=${team.name} observability=${String(team.observability)} importMetaUrl=${import.meta.url})`);
56850
+ if (team.observability !== true) {
56851
+ marker(`SKIP: observability=${String(team.observability)} !== true`);
56852
+ return;
56853
+ }
56854
+ const samplerPath = perfScriptPath("resource-sampler.mjs");
56855
+ if (!samplerPath) {
56856
+ marker(`SKIP: resource-sampler.mjs not found (importMetaUrl=${import.meta.url})`);
56857
+ return;
56858
+ }
56859
+ marker(`spawning sampler from ${samplerPath}`);
56860
+ const crewRoot = path72.dirname(path72.dirname(path72.dirname(manifest.stateRoot)));
56861
+ const outPath = path72.join(manifest.artifactsRoot, "resources.jsonl");
56862
+ const logPath = path72.join(manifest.artifactsRoot, "perf-obs.log");
56863
+ try {
56864
+ const child = spawn6(
56865
+ process.execPath,
56866
+ [
56867
+ "--experimental-strip-types",
56868
+ samplerPath,
56869
+ "--watch-run",
56870
+ manifest.runId,
56871
+ "--crew-root",
56872
+ crewRoot,
56873
+ "--interval",
56874
+ String(OBSERVABILITY_INTERVAL_MS),
56875
+ "--out",
56876
+ outPath
56877
+ ],
56878
+ { detached: true, stdio: ["ignore", "ignore", "pipe"] }
56879
+ );
56880
+ child.stderr?.on("data", (d) => {
56881
+ try {
56882
+ fs86.appendFileSync(logPath, String(d));
56883
+ } catch {
56884
+ }
56885
+ });
56886
+ child.unref();
56887
+ } catch (err2) {
56888
+ console.warn(`[perf-obs] sampler spawn failed for ${manifest.runId}: ${String(err2)}`);
56889
+ }
56890
+ }
56891
+ function schedulePerfAnalyze(manifest, team) {
56892
+ if (team.observability !== true) return;
56893
+ const analyzePath = perfScriptPath("analyze-run.mjs");
56894
+ const resourcesPath = path72.join(manifest.artifactsRoot, "resources.jsonl");
56895
+ if (!analyzePath || !fs86.existsSync(resourcesPath)) return;
56896
+ const crewRoot = path72.dirname(path72.dirname(path72.dirname(manifest.stateRoot)));
56897
+ const timer = setTimeout(() => {
56898
+ try {
56899
+ const child = spawn6(
56900
+ process.execPath,
56901
+ ["--experimental-strip-types", analyzePath, manifest.runId, "--crew-root", crewRoot, "--resources", resourcesPath],
56902
+ { detached: true, stdio: "ignore" }
56903
+ );
56904
+ child.unref();
56905
+ } catch (err2) {
56906
+ console.warn(`[perf-obs] analyze spawn failed for ${manifest.runId}: ${String(err2)}`);
56907
+ }
56908
+ }, OBSERVABILITY_ANALYZE_DELAY_MS);
56909
+ timer.unref();
56910
+ }
56819
56911
  function checkPerTaskBudget(tasks, budgetTotal, budgetWarning, budgetAbort, fairShareFraction = 0.5) {
56820
56912
  const usage = aggregateUsage(tasks);
56821
56913
  const totalUsed = (usage?.input ?? 0) + (usage?.output ?? 0) + (usage?.cacheWrite ?? 0);
@@ -56859,9 +56951,6 @@ function markBlocked(tasks, reason) {
56859
56951
  } : task
56860
56952
  );
56861
56953
  }
56862
- function isNonTerminalTaskStatus(status) {
56863
- return status === "queued" || status === "running" || status === "waiting";
56864
- }
56865
56954
  function cancelNonTerminalTasks(tasks, status, reason, filter, transform) {
56866
56955
  const predicate = filter ?? ((task) => isNonTerminalTaskStatus(task.status));
56867
56956
  return tasks.map((task) => {
@@ -56870,63 +56959,6 @@ function cancelNonTerminalTasks(tasks, status, reason, filter, transform) {
56870
56959
  return transform ? transform(task, terminalised) : terminalised;
56871
56960
  });
56872
56961
  }
56873
- function safeFinishedAt(task) {
56874
- if (!task.finishedAt) return -Infinity;
56875
- const ms = new Date(task.finishedAt).getTime();
56876
- return Number.isNaN(ms) ? Infinity : ms;
56877
- }
56878
- function isMalformedFinishedAtReplacement(currentTime, updatedTime) {
56879
- return !Number.isFinite(currentTime) && Number.isFinite(updatedTime);
56880
- }
56881
- function statusMergeKey(from, to) {
56882
- return `${from}->${to}`;
56883
- }
56884
- function shouldMergeTaskUpdate(current, updated) {
56885
- if (REJECTED_STATUS_MERGE_TRANSITIONS.has(statusMergeKey(current.status, updated.status))) return false;
56886
- if (current.status === updated.status && updated.status === "running" && current.resultArtifact && !updated.resultArtifact)
56887
- return false;
56888
- if (current.status === updated.status && current.status === "completed" && current.resultArtifact && !updated.resultArtifact)
56889
- return false;
56890
- if (current.finishedAt !== void 0 && updated.finishedAt !== void 0) {
56891
- const currentTime = safeFinishedAt(current);
56892
- const updatedTime = safeFinishedAt(updated);
56893
- if (!Number.isFinite(currentTime)) {
56894
- console.warn(`[team-runner] Task ${current.id} has malformed finishedAt: ${current.finishedAt}`);
56895
- }
56896
- if (isMalformedFinishedAtReplacement(currentTime, updatedTime)) {
56897
- return true;
56898
- }
56899
- if (updatedTime < currentTime) return false;
56900
- }
56901
- if (!updated.finishedAt && !isNonTerminalTaskStatus(updated.status)) return false;
56902
- const hasMeaningfulUpdate = updated.status !== current.status || updated.finishedAt !== current.finishedAt || updated.startedAt !== current.startedAt || Boolean(updated.resultArtifact) !== Boolean(current.resultArtifact) || Boolean(updated.resultArtifact) && updated.resultArtifact !== current.resultArtifact || Boolean(updated.error) || Boolean(updated.modelAttempts?.length) || Boolean(updated.usage) || Boolean(updated.attempts?.length) || updated.heartbeat?.lastSeenAt !== current.heartbeat?.lastSeenAt || updated.jsonEvents !== current.jsonEvents || updated.agentProgress?.lastActivityAt !== current.agentProgress?.lastActivityAt;
56903
- return hasMeaningfulUpdate;
56904
- }
56905
- function mergeTaskUpdatesPreservingTerminal(base, results) {
56906
- const indexById = /* @__PURE__ */ new Map();
56907
- for (const task of base) indexById.set(task.id, task);
56908
- let skipped = 0;
56909
- for (const result4 of results) {
56910
- for (const updated of result4.tasks) {
56911
- const current = indexById.get(updated.id);
56912
- if (!current) continue;
56913
- if (!shouldMergeTaskUpdate(current, updated)) {
56914
- console.debug("[team-runner] Skipping stale merge for task", updated.id, {
56915
- currentStatus: current.status,
56916
- updatedStatus: updated.status,
56917
- currentFinishedAt: current.finishedAt,
56918
- updatedFinishedAt: updated.finishedAt
56919
- });
56920
- skipped += 1;
56921
- continue;
56922
- }
56923
- indexById.set(updated.id, updated);
56924
- }
56925
- }
56926
- const merged = base.map((task) => indexById.get(task.id) ?? task);
56927
- void skipped;
56928
- return refreshTaskGraphQueues(merged);
56929
- }
56930
56962
  function formatTaskProgress(task) {
56931
56963
  return `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.error ? ` - ${task.error}` : ""}`;
56932
56964
  }
@@ -57188,6 +57220,7 @@ async function executeTeamRun(input) {
57188
57220
  }
57189
57221
  void registerRunPromise(manifest.runId);
57190
57222
  const stopTeamHeartbeat = startTeamRunHeartbeat(manifest.stateRoot, manifest.runId);
57223
+ startPerfSampler(manifest, input.team);
57191
57224
  const cleanupUsage = () => {
57192
57225
  for (const task of input.tasks) clearTrackedTaskUsage(task.id);
57193
57226
  };
@@ -57253,6 +57286,7 @@ async function executeTeamRun(input) {
57253
57286
  );
57254
57287
  }
57255
57288
  await flushEventLogBuffer();
57289
+ schedulePerfAnalyze(manifest, input.team);
57256
57290
  return result4;
57257
57291
  } catch (error) {
57258
57292
  stopTeamHeartbeat();
@@ -58340,15 +58374,13 @@ async function executeTeamRunCore(input, manifest, workflow) {
58340
58374
  await drainPendingUnits(pendingUnits, runController);
58341
58375
  }
58342
58376
  }
58343
- var builtInRegistry, REJECTED_STATUS_MERGE_TRANSITIONS, __test__shouldMergeTaskUpdate, __test__mergeTaskUpdates, lastProgressContentHash, __test__lastProgressContentHash, __test__writeProgress, __test__cancelPlanTasks;
58377
+ var OBSERVABILITY_INTERVAL_MS, OBSERVABILITY_ANALYZE_DELAY_MS, lastProgressContentHash, __test__lastProgressContentHash, __test__writeProgress, __test__cancelPlanTasks;
58344
58378
  var init_team_runner = __esm({
58345
58379
  "src/runtime/team-runner.ts"() {
58346
58380
  "use strict";
58347
58381
  init_errors3();
58348
58382
  init_registry2();
58349
58383
  init_correlation();
58350
- init_plugin_registry();
58351
- init_plugins();
58352
58384
  init_atomic_write();
58353
58385
  init_contracts();
58354
58386
  init_locks();
@@ -58366,6 +58398,7 @@ var init_team_runner = __esm({
58366
58398
  init_goal_achievement();
58367
58399
  init_group_join();
58368
58400
  init_live_agent_manager();
58401
+ init_merge_gate();
58369
58402
  init_runtime_policy();
58370
58403
  init_path_overlap();
58371
58404
  init_policy_engine();
@@ -58387,32 +58420,10 @@ var init_team_runner = __esm({
58387
58420
  init_usage_tracker();
58388
58421
  init_workflow_state();
58389
58422
  init_adaptive_plan();
58423
+ init_merge_gate();
58390
58424
  init_adaptive_plan();
58391
- builtInRegistry = new PluginRegistry();
58392
- builtInRegistry.register(NextJsPlugin);
58393
- builtInRegistry.register(VitestPlugin);
58394
- builtInRegistry.register(VitePlugin);
58395
- REJECTED_STATUS_MERGE_TRANSITIONS = (() => {
58396
- const rejected = /* @__PURE__ */ new Set();
58397
- for (const from of TEAM_TASK_STATUSES) {
58398
- if (!TEAM_TERMINAL_TASK_STATUSES.has(from)) continue;
58399
- for (const to of TEAM_TASK_STATUSES) {
58400
- if (!TEAM_TERMINAL_TASK_STATUSES.has(to)) rejected.add(statusMergeKey(from, to));
58401
- }
58402
- }
58403
- rejected.add(statusMergeKey("waiting", "running"));
58404
- const completedIntegrityFlips = [
58405
- ["completed", "failed"],
58406
- ["completed", "needs_attention"],
58407
- ["failed", "completed"],
58408
- ["cancelled", "completed"],
58409
- ["needs_attention", "completed"]
58410
- ];
58411
- for (const [from, to] of completedIntegrityFlips) rejected.add(statusMergeKey(from, to));
58412
- return rejected;
58413
- })();
58414
- __test__shouldMergeTaskUpdate = shouldMergeTaskUpdate;
58415
- __test__mergeTaskUpdates = mergeTaskUpdatesPreservingTerminal;
58425
+ OBSERVABILITY_INTERVAL_MS = 2e3;
58426
+ OBSERVABILITY_ANALYZE_DELAY_MS = 3e3;
58416
58427
  lastProgressContentHash = /* @__PURE__ */ new Map();
58417
58428
  __test__lastProgressContentHash = lastProgressContentHash;
58418
58429
  __test__writeProgress = writeProgress;
@@ -66261,7 +66272,6 @@ var init_mascot = __esm({
66261
66272
  currentArminGrid;
66262
66273
  effectState = {};
66263
66274
  effectDone = false;
66264
- visible = true;
66265
66275
  frame = 0;
66266
66276
  effectPhase = 0;
66267
66277
  gridVersion = 0;
@@ -66357,7 +66367,7 @@ var init_mascot = __esm({
66357
66367
  this.gridVersion++;
66358
66368
  }
66359
66369
  this.invalidate();
66360
- if (this.visible) this.requestRender?.();
66370
+ this.requestRender?.();
66361
66371
  }
66362
66372
  tickArminEffect() {
66363
66373
  switch (this.effect) {
@@ -66564,14 +66574,6 @@ var init_mascot = __esm({
66564
66574
  this.close();
66565
66575
  }
66566
66576
  }
66567
- /**
66568
- * Set whether the mascot is currently visible (not obscured by another
66569
- * overlay). When invisible, tick() skips requestRender so the animation
66570
- * does not trigger needless repaints while hidden.
66571
- */
66572
- setVisible(visible) {
66573
- this.visible = visible;
66574
- }
66575
66577
  dispose() {
66576
66578
  this.doneGuard.called = true;
66577
66579
  if (this.interval) clearInterval(this.interval);
@@ -70620,10 +70622,18 @@ var init_otlp_exporter = __esm({
70620
70622
  });
70621
70623
 
70622
70624
  // src/observability/metrics-primitives.ts
70623
- function enforceLabelCap(map3, metricName) {
70625
+ function getCardinalityEvictions() {
70626
+ return cardinalityEvictions;
70627
+ }
70628
+ function enforceLabelCap(map3, _metricName) {
70624
70629
  while (map3.size > MAX_LABEL_COMBINATIONS) {
70625
70630
  const firstKey = map3.keys().next().value;
70626
- if (firstKey !== void 0) map3.delete(firstKey);
70631
+ if (firstKey !== void 0) {
70632
+ map3.delete(firstKey);
70633
+ cardinalityEvictions++;
70634
+ } else {
70635
+ break;
70636
+ }
70627
70637
  }
70628
70638
  }
70629
70639
  function normalizeLabels(labels = {}) {
@@ -70637,12 +70647,13 @@ function labelKey(labels = {}) {
70637
70647
  function cloneLabels(labels) {
70638
70648
  return { ...labels };
70639
70649
  }
70640
- var DEFAULT_HISTOGRAM_BUCKETS, MAX_LABEL_COMBINATIONS, Metric, Counter, Gauge, Histogram;
70650
+ var DEFAULT_HISTOGRAM_BUCKETS, MAX_LABEL_COMBINATIONS, cardinalityEvictions, Metric, Counter, Gauge, Histogram;
70641
70651
  var init_metrics_primitives = __esm({
70642
70652
  "src/observability/metrics-primitives.ts"() {
70643
70653
  "use strict";
70644
70654
  DEFAULT_HISTOGRAM_BUCKETS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4];
70645
70655
  MAX_LABEL_COMBINATIONS = 1e4;
70656
+ cardinalityEvictions = 0;
70646
70657
  Metric = class {
70647
70658
  name;
70648
70659
  description;
@@ -70907,6 +70918,14 @@ function wireEventToMetrics(events, registry2) {
70907
70918
  const deadletterCount = registry2.counter("crew.task.deadletter_total", "Deadletter triggers by reason");
70908
70919
  const overflowCount = registry2.counter("crew.task.overflow_phase_total", "Overflow recovery phase transitions");
70909
70920
  const supervisorContactCount = registry2.counter("crew.task.supervisor_contact_total", "Supervisor contact requests by reason");
70921
+ const unboundedConcurrencyCount = registry2.counter(
70922
+ "crew.limits.unbounded_total",
70923
+ "Runs that enabled allowUnboundedConcurrency (advisory; bypasses hard cap)"
70924
+ );
70925
+ const cardinalityEvictedGauge = registry2.gauge(
70926
+ "crew.metrics.cardinality_evicted",
70927
+ "Cumulative label-combination evictions (non-zero = unreliable aggregation)"
70928
+ );
70910
70929
  registry2.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds");
70911
70930
  const runDuration = registry2.histogram(
70912
70931
  "crew.run.duration_ms",
@@ -71006,11 +71025,21 @@ function wireEventToMetrics(events, registry2) {
71006
71025
  direction: stringValue(item.direction, "unknown")
71007
71026
  });
71008
71027
  }
71028
+ ],
71029
+ [
71030
+ "crew.limits.unbounded",
71031
+ () => {
71032
+ unboundedConcurrencyCount.inc({});
71033
+ }
71009
71034
  ]
71010
71035
  ];
71011
71036
  const unsubscribers = [];
71012
71037
  for (const [event, handler] of handlers) {
71013
71038
  const unsubscribe = events?.on?.(event, (data) => {
71039
+ try {
71040
+ cardinalityEvictedGauge.set({}, getCardinalityEvictions());
71041
+ } catch {
71042
+ }
71014
71043
  try {
71015
71044
  handler(data);
71016
71045
  } catch {
@@ -71031,6 +71060,7 @@ var CANCELLATION_REASON_LABELS;
71031
71060
  var init_event_to_metric = __esm({
71032
71061
  "src/observability/event-to-metric.ts"() {
71033
71062
  "use strict";
71063
+ init_metrics_primitives();
71034
71064
  CANCELLATION_REASON_LABELS = /* @__PURE__ */ new Set([
71035
71065
  "caller_cancelled",
71036
71066
  "leader_interrupted",
@@ -72583,7 +72613,7 @@ function isDangerStage(index, levels) {
72583
72613
  init_pi_ui_compat();
72584
72614
  init_theme_adapter();
72585
72615
  init_visual();
72586
- import { isAbsolute as isAbsolute10, relative as relative8, resolve as resolve21, sep as sep9 } from "node:path";
72616
+ import { isAbsolute as isAbsolute11, relative as relative8, resolve as resolve21, sep as sep9 } from "node:path";
72587
72617
 
72588
72618
  // src/extension/crew-vibes/render.ts
72589
72619
  function formatCount(value) {
@@ -72720,7 +72750,7 @@ function formatCwdForFooter(cwd, home) {
72720
72750
  const resolvedCwd = resolve21(cwd);
72721
72751
  const resolvedHome = resolve21(home);
72722
72752
  const rel = relative8(resolvedHome, resolvedCwd);
72723
- const inside = rel === "" || rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute10(rel);
72753
+ const inside = rel === "" || rel !== ".." && !rel.startsWith(`..${sep9}`) && !isAbsolute11(rel);
72724
72754
  if (!inside) return cwd;
72725
72755
  return rel === "" ? "~" : `~${sep9}${rel}`;
72726
72756
  }
@@ -75285,7 +75315,7 @@ function startForegroundRunImpl(pi, ctx, extensionCtx, runner, runId) {
75285
75315
  init_config();
75286
75316
  import * as fs103 from "node:fs";
75287
75317
  import * as path82 from "node:path";
75288
- import { fileURLToPath as fileURLToPath7 } from "node:url";
75318
+ import { fileURLToPath as fileURLToPath8 } from "node:url";
75289
75319
 
75290
75320
  // src/runtime/per-write-validator.ts
75291
75321
  import { readFileSync as readFileSync81 } from "node:fs";
@@ -75388,7 +75418,7 @@ function installResourcesDiscoverHook(pi, ctx) {
75388
75418
  pi.on("resources_discover", () => {
75389
75419
  const sessionCwd = ctx.currentCtx?.cwd ?? process.cwd();
75390
75420
  const skillDir = path82.resolve(sessionCwd, "skills");
75391
- const extSkillDir = path82.resolve(path82.dirname(fileURLToPath7(import.meta.url)), "..", "..", "skills");
75421
+ const extSkillDir = path82.resolve(path82.dirname(fileURLToPath8(import.meta.url)), "..", "..", "skills");
75392
75422
  const paths = [];
75393
75423
  if (fs103.existsSync(extSkillDir)) paths.push(extSkillDir);
75394
75424
  if (skillDir !== extSkillDir && fs103.existsSync(skillDir)) {
@@ -76011,19 +76041,6 @@ var CrewBroker = class {
76011
76041
  }
76012
76042
  this.resolvedSocketPath = null;
76013
76043
  }
76014
- /**
76015
- * Non-throwing enqueue entry point for the post-append mailbox observer
76016
- * (Phase 1) or any other in-process producer. Phase 0 accepts `notifyMessage`
76017
- * as a no-op shape so the lifecycle controller can install a single
76018
- * observer regardless of broker state.
76019
- *
76020
- * Fanout goes ONLY to authenticated connections matching the recipient.
76021
- * Phase 0 keeps this as a typed no-op (`not-implemented` would be
76022
- * inappropriate here — the caller is in-process and shouldn't be
76023
- * punished for testing the broker skeleton).
76024
- */
76025
- notifyMessage(_message) {
76026
- }
76027
76044
  // ------------------------------------------------------------------------
76028
76045
  // Connection lifecycle
76029
76046
  // ------------------------------------------------------------------------