@wrongstack/core 0.302.2 → 0.305.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/dist/chronicle/index.js +117 -30
  3. package/dist/chronicle/project-server.js +12 -5
  4. package/dist/coordination/agents/index.js +4645 -3448
  5. package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
  6. package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
  7. package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
  8. package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
  9. package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
  10. package/dist/coordination/agents/project-agent-files.d.ts +12 -3
  11. package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
  12. package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
  13. package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
  14. package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
  15. package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
  16. package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
  17. package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
  18. package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
  19. package/dist/coordination/agents/role-skills.d.ts +11 -1
  20. package/dist/coordination/agents/types.d.ts +10 -2
  21. package/dist/coordination/director-prompts.d.ts +19 -6
  22. package/dist/coordination/director-tools.d.ts +2 -2
  23. package/dist/coordination/fleet.d.ts +0 -6
  24. package/dist/coordination/index.d.ts +1 -1
  25. package/dist/coordination/index.js +5746 -4326
  26. package/dist/coordination/mail-tools.d.ts +1 -1
  27. package/dist/core/agent-types.d.ts +4 -2
  28. package/dist/core/agent.d.ts +1 -0
  29. package/dist/core/context.d.ts +19 -0
  30. package/dist/core/conversation-state.d.ts +14 -0
  31. package/dist/core/fallback-profile-manager.d.ts +70 -2
  32. package/dist/core/index.js +317 -108
  33. package/dist/core/system-prompt-blocks.d.ts +1 -1
  34. package/dist/core/system-prompt-builder.d.ts +13 -1
  35. package/dist/core/system-prompt-glossary.d.ts +73 -0
  36. package/dist/core/system-prompt-memory-skills.d.ts +2 -2
  37. package/dist/defaults/index.js +1635 -1120
  38. package/dist/execution/council-orchestrator.d.ts +3 -13
  39. package/dist/execution/index.js +3136 -2706
  40. package/dist/execution/one-shot-llm.d.ts +5 -0
  41. package/dist/goal/index.js +7 -0
  42. package/dist/hq/index.js +17 -7
  43. package/dist/hq/protocol/kanban.d.ts +21 -0
  44. package/dist/hq/protocol.js +5 -1
  45. package/dist/hq/redaction.d.ts +14 -0
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +5502 -3483
  48. package/dist/infrastructure/index.js +247 -122
  49. package/dist/kernel/events/agent-events.d.ts +28 -0
  50. package/dist/plugin/index.js +111 -7
  51. package/dist/registry/index.js +11 -0
  52. package/dist/registry/tool-registry.d.ts +8 -0
  53. package/dist/replay/hash.d.ts +9 -0
  54. package/dist/replay/index.js +14 -4
  55. package/dist/replay/replay-provider-runner.d.ts +31 -1
  56. package/dist/security/index.js +92 -21
  57. package/dist/security/kanban-boundary.d.ts +5 -1
  58. package/dist/security/secret-vault.d.ts +2 -0
  59. package/dist/session-catalog/index.js +86 -10
  60. package/dist/session-catalog/project-server.js +100 -17
  61. package/dist/session-catalog/protocol.d.ts +20 -4
  62. package/dist/session-catalog/store.d.ts +19 -3
  63. package/dist/storage/index.js +305 -69
  64. package/dist/storage/memory-consolidator.d.ts +4 -2
  65. package/dist/storage/plan-store.d.ts +1 -1
  66. package/dist/storage/session-resume-validation.d.ts +24 -0
  67. package/dist/storage/session-store/directory-scan.d.ts +5 -1
  68. package/dist/storage/session-store/fork-session.d.ts +13 -1
  69. package/dist/storage/session-store/load-cache.d.ts +11 -0
  70. package/dist/storage/session-store/prune-helpers.d.ts +5 -0
  71. package/dist/storage/session-store.d.ts +18 -0
  72. package/dist/tasking/index.js +5 -0
  73. package/dist/tools/index.js +1151 -831
  74. package/dist/types/config/mcp-features.d.ts +31 -1
  75. package/dist/types/config/root.d.ts +23 -1
  76. package/dist/types/config/skills-fleet-brain.d.ts +34 -0
  77. package/dist/types/config/tools.d.ts +22 -0
  78. package/dist/types/config/ui.d.ts +14 -0
  79. package/dist/types/config.d.ts +1 -0
  80. package/dist/types/default-config.d.ts +1 -0
  81. package/dist/types/index.d.ts +2 -2
  82. package/dist/types/index.js +23 -0
  83. package/dist/types/multi-agent.d.ts +7 -0
  84. package/dist/types/session.d.ts +9 -1
  85. package/dist/types/task-graph.d.ts +2 -0
  86. package/dist/types/tool-executor.d.ts +2 -0
  87. package/dist/utils/index.d.ts +1 -0
  88. package/dist/utils/index.js +217 -76
  89. package/dist/utils/project-state-guard.d.ts +21 -0
  90. package/dist/utils/session-scoped-path.d.ts +17 -0
  91. package/dist/utils/todos-format.d.ts +20 -0
  92. package/instructions/leader-after-task.md +3 -4
  93. package/instructions/system-lite.md +17 -14
  94. package/instructions/system-pro.md +29 -30
  95. package/instructions/system.md +29 -28
  96. package/package.json +3 -3
  97. package/skills/wrongstack-kanban/SKILL.md +95 -93
@@ -1277,8 +1277,8 @@ function defaultFormatTaskInput(task) {
1277
1277
  }
1278
1278
 
1279
1279
  // src/coordination/agents/agent-prompts.ts
1280
- import { readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
1281
- import * as path5 from "node:path";
1280
+ import { readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
1281
+ import * as path6 from "node:path";
1282
1282
  import { fileURLToPath } from "node:url";
1283
1283
 
1284
1284
  // src/utils/wstack-paths.ts
@@ -1421,8 +1421,8 @@ function resolveWstackPaths(opts) {
1421
1421
  }
1422
1422
 
1423
1423
  // src/coordination/agents/project-agent-identity.ts
1424
- import { existsSync as existsSync2, readFileSync as readFileSync4 } from "node:fs";
1425
- import * as path4 from "node:path";
1424
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "node:fs";
1425
+ import * as path5 from "node:path";
1426
1426
 
1427
1427
  // src/types/runtime-capability-manifest.ts
1428
1428
  var PLAYWRIGHT_ALIASES = {
@@ -1680,22 +1680,6 @@ function inferRuntimeCapabilities(toolNames) {
1680
1680
  return [...ids];
1681
1681
  }
1682
1682
 
1683
- // src/coordination/agents/project-agent-consolidation.ts
1684
- import {
1685
- existsSync,
1686
- mkdirSync,
1687
- readFileSync as readFileSync2,
1688
- rmSync
1689
- } from "node:fs";
1690
- import * as path3 from "node:path";
1691
-
1692
- // src/coordination/agents/project-agent-learning-entries.ts
1693
- function splitLearnedEntries(body) {
1694
- return body.split(/\n---\n+/).map((entry) => entry.trim()).map(
1695
- (entry) => entry.replace(/^# Learned wisdom for .+$/im, "").replace(/<!--[\s\S]*?-->/g, "").trim()
1696
- ).filter(Boolean);
1697
- }
1698
-
1699
1683
  // src/coordination/agents/project-agent-paths.ts
1700
1684
  import * as path2 from "node:path";
1701
1685
  var AGENT_ROLE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,95}$/i;
@@ -1717,6 +1701,201 @@ function learningPolicyPath(role, projectRoot) {
1717
1701
  return path2.join(roleDir(role, projectRoot), "learning.json");
1718
1702
  }
1719
1703
 
1704
+ // src/coordination/agents/project-agent-consolidation.ts
1705
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync } from "node:fs";
1706
+ import * as path3 from "node:path";
1707
+
1708
+ // src/coordination/agents/project-agent-learning-entries.ts
1709
+ function splitLearnedEntries(body) {
1710
+ return body.split(/\n---\n+/).map((entry) => entry.trim()).map(
1711
+ (entry) => entry.replace(/^#\s+Learned (?:wisdom|instructions) for .+$/im, "").replace(/^>\s*Project-specific learning data for[\s\S]*?$/im, "").replace(/^_No learned entries yet\._$/im, "").replace(/^\*Last capture:[^\n]*\*$/im, "").replace(/<!--[\s\S]*?-->/g, "").trim()
1712
+ ).filter(Boolean);
1713
+ }
1714
+
1715
+ // src/coordination/agents/project-agent-learning-normalize.ts
1716
+ var MIN_INSTRUCTIVE_LENGTH = 30;
1717
+ function classifyLearnedEntry(text) {
1718
+ const lower = text.toLowerCase();
1719
+ if (/\b(?:avoid|never|don'?t|do not|must not|prevent|watch out|beware|pitfall|gotcha|hazard|risk)\b/.test(
1720
+ lower
1721
+ ))
1722
+ return "warning";
1723
+ if (/\b(?:use|prefer|choose|adopt|leverage|apply|switch to|migrate to)\b/.test(lower) || /\b(?:pattern|approach|strategy|technique|idiom)\b/.test(lower))
1724
+ return "pattern";
1725
+ if (/\b(?:always|must|should|require|ensure|verify|check|run|execute|before|after|when)\b/.test(
1726
+ lower
1727
+ ))
1728
+ return "convention";
1729
+ return "fact";
1730
+ }
1731
+ function normalizeForComparison(text) {
1732
+ return text.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim().split(/\s+/).sort().join(" ");
1733
+ }
1734
+
1735
+ // src/coordination/agents/project-agent-learning-structured.ts
1736
+ function directiveTrials(entry) {
1737
+ const count = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
1738
+ const applied = count(entry.applied);
1739
+ const wins = Math.min(applied, count(entry.wins));
1740
+ return { applied, wins, losses: applied - wins };
1741
+ }
1742
+ function parseLearnedEntryStamp(entry) {
1743
+ const structuredMatch = entry.match(
1744
+ /<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
1745
+ );
1746
+ if (structuredMatch) {
1747
+ const rawCategory = structuredMatch[1];
1748
+ const candidate = parseLearnedCategory(rawCategory);
1749
+ return {
1750
+ capturedAt: typeof structuredMatch[2] === "string" ? structuredMatch[2].trim() : "",
1751
+ category: candidate ?? null
1752
+ };
1753
+ }
1754
+ const stampMatch = entry.match(/^>\s*(?:\[\s*([\w-]+)\s*\]\s+)?(?:Captured|Taught)\s+(\S+)/m);
1755
+ if (!stampMatch) {
1756
+ return { capturedAt: "", category: null };
1757
+ }
1758
+ return {
1759
+ capturedAt: typeof stampMatch[2] === "string" ? stampMatch[2] : "",
1760
+ category: parseLearnedCategory(stampMatch[1]) ?? null
1761
+ };
1762
+ }
1763
+ function parseLearnedCategory(value) {
1764
+ return value === "convention" || value === "pattern" || value === "warning" || value === "fact" ? value : void 0;
1765
+ }
1766
+ function stripStamp(entry) {
1767
+ return entry.replace(/<!--\s*learned-stamp:[\s\S]*?-->/g, "").replace(/^>\s*(?:\[[\w-]+\]\s+)?(?:Captured|Taught)\s+.+$/m, "").replace(/\n{3,}/g, "\n\n").trim();
1768
+ }
1769
+ function directiveKey(text) {
1770
+ return normalizeForComparison(text);
1771
+ }
1772
+ function decomposeLearnedEntry(text, category) {
1773
+ const what = text;
1774
+ const why = deriveWhy(category, text);
1775
+ const how = extractHow(text);
1776
+ return { what, why, how };
1777
+ }
1778
+ var WHY_BY_CATEGORY = {
1779
+ convention: "Established convention for this codebase \u2014 skipping it risks regressions, merge friction, or out-of-sync state with peers.",
1780
+ pattern: "This project's chosen approach \u2014 alternatives were considered and either conflict with existing architecture or were rejected for known reasons.",
1781
+ warning: "Known failure mode \u2014 skipping this has caused real defects in this codebase. The cost of getting it wrong outweighs the cost of the check.",
1782
+ fact: "Current state of the project \u2014 assumed by other conventions, build steps, or peers, so acting on a stale assumption wastes a cycle."
1783
+ };
1784
+ function deriveWhy(category, text) {
1785
+ const base = WHY_BY_CATEGORY[category];
1786
+ const signals = [];
1787
+ const lower = text.toLowerCase();
1788
+ if (/\bbefore\s+(?:merge|commit|deploy|release|shipping|publishing)\b/.test(lower))
1789
+ signals.push("guard before shipping");
1790
+ if (/\bto\s+avoid\b/.test(lower)) {
1791
+ const m = text.match(/to avoid ([^.!?]+)/i);
1792
+ if (m?.[1]) signals.push(`avoid ${m[1].trim()}`);
1793
+ }
1794
+ if (/\bso\s+(?:that|we|the project)\b/.test(lower)) {
1795
+ const m = text.match(/so (?:that |we |the project )?([^.!?]+)/i);
1796
+ if (m?.[1]) signals.push(`so ${m[1].trim()}`);
1797
+ }
1798
+ if (signals.length === 0) return base;
1799
+ return `${base} Project signals: ${signals.join("; ")}.`;
1800
+ }
1801
+ function extractHow(text) {
1802
+ const anchors = /* @__PURE__ */ new Set();
1803
+ const backticked = text.match(/`([^`]+)`/g) ?? [];
1804
+ for (const raw of backticked) {
1805
+ const inner = raw.replace(/`/g, "").trim();
1806
+ if (inner.length > 0 && inner.length <= 120) anchors.add(inner);
1807
+ }
1808
+ const pathMatches = text.match(
1809
+ /(?:[a-zA-Z0-9_.-]+\/)+[a-zA-Z0-9_.-]+\.(?:tsx|jsonc|json|jsx|yaml|mjs|cjs|yml|ts|js|md)\b/g
1810
+ ) ?? [];
1811
+ for (const p of pathMatches) anchors.add(p);
1812
+ const scoped = text.match(/@[a-z0-9][\w.-]*\/[a-z0-9][\w.-]*/gi) ?? [];
1813
+ for (const p of scoped) anchors.add(p);
1814
+ if (anchors.size === 0) return "";
1815
+ return [...anchors].map((anchor) => `\`${anchor}\``).join("\n");
1816
+ }
1817
+ function cleanHowLine(line) {
1818
+ let value = line.trim();
1819
+ let previous;
1820
+ do {
1821
+ previous = value;
1822
+ value = value.replace(/^[-*]\s+/, "").replace(/^\*How:\*\s*/i, "").trim();
1823
+ } while (value !== previous);
1824
+ return value;
1825
+ }
1826
+ function parseStampAttributes(body) {
1827
+ const attributes = {};
1828
+ for (const part of body.split(";")) {
1829
+ const eq = part.indexOf("=");
1830
+ if (eq === -1) continue;
1831
+ const key = part.slice(0, eq).trim();
1832
+ const value = part.slice(eq + 1).trim();
1833
+ if (key) attributes[key] = value;
1834
+ }
1835
+ return attributes;
1836
+ }
1837
+ function parseEntryBody(body) {
1838
+ const lines = [];
1839
+ for (const line of body.split("\n")) {
1840
+ if (/^##\s/.test(line) || /^---\s*$/.test(line) || /^<!--/.test(line)) break;
1841
+ lines.push(line);
1842
+ }
1843
+ const text = lines.join("\n");
1844
+ const what = /^-\s+\*\*([\s\S]+?)\*\*\s*$/m.exec(text)?.[1]?.replace(/\s+/g, " ").trim() ?? "";
1845
+ if (!what) return void 0;
1846
+ const why = /^\s+-\s+\*Why:\*\s+(.+)$/m.exec(text)?.[1]?.trim() ?? "";
1847
+ const how = lines.filter((line) => /^\s+-\s+\*How:\*/i.test(line)).map((line) => cleanHowLine(line)).filter(Boolean).join("\n");
1848
+ return { what, why, how };
1849
+ }
1850
+ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLearnedEntries(raw)) {
1851
+ if (!raw) return [];
1852
+ const structured = [];
1853
+ const stampPattern = /<!--\s*learned-stamp:\s*([^>]*?)\s*-->/g;
1854
+ const stamps = [...raw.matchAll(stampPattern)];
1855
+ for (const [index, stamp] of stamps.entries()) {
1856
+ const attributes = parseStampAttributes(stamp[1] ?? "");
1857
+ const category = parseLearnedCategory(attributes["category"]) ?? "fact";
1858
+ const capturedAt = attributes["capturedAt"] ?? "";
1859
+ const skill = attributes["skill"];
1860
+ const trials = directiveTrials({
1861
+ applied: Number(attributes["applied"]),
1862
+ wins: Number(attributes["wins"])
1863
+ });
1864
+ const start = (stamp.index ?? 0) + stamp[0].length;
1865
+ const end = stamps[index + 1]?.index ?? raw.length;
1866
+ const parsed = parseEntryBody(raw.slice(start, end));
1867
+ if (!parsed || parsed.what.length < MIN_INSTRUCTIVE_LENGTH) continue;
1868
+ structured.push({
1869
+ key: directiveKey(parsed.what),
1870
+ category,
1871
+ what: parsed.what,
1872
+ why: parsed.why || WHY_BY_CATEGORY[category],
1873
+ how: parsed.how,
1874
+ capturedAt,
1875
+ ...skill ? { skill } : {},
1876
+ ...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
1877
+ });
1878
+ }
1879
+ if (structured.length === 0) {
1880
+ for (const chunk of legacyEntries) {
1881
+ const stamp = parseLearnedEntryStamp(chunk);
1882
+ const directive = stamp.capturedAt || stamp.category ? stripStamp(chunk) : chunk;
1883
+ if (directive.length < MIN_INSTRUCTIVE_LENGTH) continue;
1884
+ const category = stamp.category ?? classifyLearnedEntry(directive);
1885
+ const { what, why, how } = decomposeLearnedEntry(directive, category);
1886
+ structured.push({
1887
+ key: directiveKey(directive),
1888
+ category,
1889
+ what,
1890
+ why,
1891
+ how,
1892
+ capturedAt: stamp.capturedAt
1893
+ });
1894
+ }
1895
+ }
1896
+ return structured;
1897
+ }
1898
+
1720
1899
  // src/coordination/agents/project-agent-consolidation.ts
1721
1900
  function consolidationPath(role, projectRoot) {
1722
1901
  return path3.join(roleDir(role, projectRoot), "consolidated.md");
@@ -1824,34 +2003,325 @@ function loadProjectAgentLearningPolicy(role, projectRoot) {
1824
2003
  enabled: parsed.enabled !== false,
1825
2004
  lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
1826
2005
  ...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
1827
- ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {}
2006
+ ...parsed.lastCaptureSource === "automatic" || parsed.lastCaptureSource === "manual" || parsed.lastCaptureSource === "taught" ? { lastCaptureSource: parsed.lastCaptureSource } : {},
2007
+ ...typeof parsed.lastOptimizeAt === "string" ? { lastOptimizeAt: parsed.lastOptimizeAt } : {}
1828
2008
  };
1829
2009
  } catch {
1830
2010
  return { ...DEFAULT_LEARNING_POLICY };
1831
2011
  }
1832
2012
  }
1833
2013
 
2014
+ // src/coordination/agents/project-agent-skill-layer.ts
2015
+ import { readdirSync, readFileSync as readFileSync4, rmSync as rmSync2 } from "node:fs";
2016
+ import * as path4 from "node:path";
2017
+
2018
+ // src/coordination/agents/role-skills.ts
2019
+ var skillSet = (...names) => names;
2020
+ var ROLE_SKILL_SETS = {
2021
+ explore: skillSet("research-web", "node-modern", "typescript-strict"),
2022
+ search: skillSet("bug-hunter", "typescript-strict", "research-web"),
2023
+ research: skillSet("research-web", "tech-stack", "security-scanner", "api-design"),
2024
+ analyst: skillSet("sdd", "api-design", "testing", "security-scanner"),
2025
+ planner: skillSet("sdd", "multi-agent", "refactor-planner", "testing"),
2026
+ architect: skillSet("sdd", "api-design", "refactor-planner", "security-scanner", "observability"),
2027
+ critic: skillSet("chimera", "bug-hunter", "security-scanner", "refactor-planner"),
2028
+ "refactor-planner": skillSet(
2029
+ "refactor-planner",
2030
+ "sdd",
2031
+ "typescript-strict",
2032
+ "testing",
2033
+ "bug-hunter"
2034
+ ),
2035
+ executor: skillSet("sdd", "typescript-strict", "node-modern", "testing", "git-flow"),
2036
+ refactor: skillSet("refactor-planner", "typescript-strict", "testing", "bug-hunter"),
2037
+ simplifier: skillSet("bug-hunter", "typescript-strict", "testing", "refactor-planner"),
2038
+ migration: skillSet("tech-stack", "node-modern", "typescript-strict", "testing", "git-flow"),
2039
+ vision: skillSet("react-modern", "testing", "typescript-strict", "security-scanner"),
2040
+ debugger: skillSet("bug-hunter", "testing", "typescript-strict", "observability", "audit-log"),
2041
+ tracer: skillSet("observability", "audit-log", "bug-hunter", "testing"),
2042
+ verifier: skillSet("testing", "typescript-strict", "security-scanner", "chimera"),
2043
+ test: skillSet("testing", "typescript-strict", "bug-hunter", "security-scanner"),
2044
+ e2e: skillSet("testing", "react-modern", "api-design", "security-scanner"),
2045
+ browser: skillSet("testing", "react-modern", "security-scanner", "research-web"),
2046
+ performance: skillSet("observability", "testing", "node-modern", "audit-log"),
2047
+ chaos: skillSet("testing", "observability", "security-scanner", "audit-log"),
2048
+ "security-scanner": skillSet("security-scanner", "bug-hunter", "api-design", "tech-stack"),
2049
+ "bug-hunter": skillSet("bug-hunter", "typescript-strict", "testing", "security-scanner"),
2050
+ "audit-log": skillSet("audit-log", "observability", "bug-hunter", "output-standards"),
2051
+ reviewer: skillSet("chimera", "bug-hunter", "security-scanner", "testing"),
2052
+ "code-reviewer": skillSet("chimera", "bug-hunter", "typescript-strict", "testing"),
2053
+ "security-reviewer": skillSet("security-scanner", "chimera", "api-design", "tech-stack"),
2054
+ accessibility: skillSet("react-modern", "testing", "chimera", "security-scanner"),
2055
+ compliance: skillSet("security-scanner", "audit-log", "tech-stack", "chimera"),
2056
+ database: skillSet("api-design", "testing", "security-scanner", "observability"),
2057
+ api: skillSet("api-design", "typescript-strict", "node-modern", "testing", "security-scanner"),
2058
+ auth: skillSet("security-scanner", "api-design", "testing", "typescript-strict", "audit-log"),
2059
+ data: skillSet("typescript-strict", "testing", "observability", "security-scanner"),
2060
+ frontend: skillSet("react-modern", "typescript-strict", "testing", "security-scanner"),
2061
+ backend: skillSet("node-modern", "typescript-strict", "api-design", "testing", "observability"),
2062
+ designer: skillSet("react-modern", "prompt-engineering", "research-web", "output-standards"),
2063
+ ios: skillSet("sdd", "testing", "security-scanner", "research-web"),
2064
+ document: skillSet("output-standards", "research-web", "api-design", "prompt-engineering"),
2065
+ uml: skillSet("sdd", "refactor-planner", "api-design", "output-standards"),
2066
+ i18n: skillSet("react-modern", "testing", "typescript-strict", "output-standards"),
2067
+ prompt: skillSet("prompt-engineering", "skill-creator", "output-standards", "testing"),
2068
+ git: skillSet("git-flow", "chimera", "testing", "security-scanner"),
2069
+ release: skillSet("git-flow", "tech-stack", "chimera", "security-scanner"),
2070
+ devops: skillSet("docker-deploy", "observability", "security-scanner", "git-flow", "tech-stack"),
2071
+ observability: skillSet("observability", "node-modern", "testing", "audit-log"),
2072
+ dependency: skillSet("tech-stack", "security-scanner", "node-modern", "git-flow"),
2073
+ "skill-manage": skillSet(
2074
+ "skill-creator",
2075
+ "prompt-engineering",
2076
+ "security-scanner",
2077
+ "output-standards"
2078
+ ),
2079
+ "self-improving": skillSet(
2080
+ "audit-log",
2081
+ "mnemosyne",
2082
+ "prompt-engineering",
2083
+ "bug-hunter",
2084
+ "observability"
2085
+ ),
2086
+ context: skillSet("mnemosyne", "output-standards", "prompt-engineering", "audit-log"),
2087
+ cost: skillSet("audit-log", "tech-stack", "observability", "research-web"),
2088
+ "tech-stack": skillSet("tech-stack", "research-web", "security-scanner", "node-modern"),
2089
+ // ── Wave 1: Platform and systems engineering ──────────────────────────
2090
+ android: skillSet(
2091
+ "react-modern",
2092
+ "typescript-strict",
2093
+ "testing",
2094
+ "security-scanner",
2095
+ "tech-stack"
2096
+ ),
2097
+ desktop: skillSet(
2098
+ "node-modern",
2099
+ "typescript-strict",
2100
+ "testing",
2101
+ "security-scanner",
2102
+ "tech-stack"
2103
+ ),
2104
+ realtime: skillSet(
2105
+ "api-design",
2106
+ "observability",
2107
+ "node-modern",
2108
+ "typescript-strict",
2109
+ "testing",
2110
+ "tech-stack"
2111
+ ),
2112
+ "distributed-systems": skillSet(
2113
+ "api-design",
2114
+ "observability",
2115
+ "typescript-strict",
2116
+ "testing",
2117
+ "security-scanner",
2118
+ "tech-stack"
2119
+ ),
2120
+ concurrency: skillSet(
2121
+ "bug-hunter",
2122
+ "typescript-strict",
2123
+ "testing",
2124
+ "observability",
2125
+ "node-modern",
2126
+ "tech-stack"
2127
+ ),
2128
+ "platform-engineer": skillSet(
2129
+ "node-modern",
2130
+ "typescript-strict",
2131
+ "git-flow",
2132
+ "docker-deploy",
2133
+ "tech-stack"
2134
+ ),
2135
+ // ── Wave 3: Product and data specialists ──────────────────────────────
2136
+ payments: skillSet(
2137
+ "api-design",
2138
+ "security-scanner",
2139
+ "audit-log",
2140
+ "testing",
2141
+ "typescript-strict",
2142
+ "tech-stack"
2143
+ ),
2144
+ messaging: skillSet(
2145
+ "api-design",
2146
+ "observability",
2147
+ "node-modern",
2148
+ "testing",
2149
+ "security-scanner",
2150
+ "tech-stack"
2151
+ ),
2152
+ "search-relevance": skillSet(
2153
+ "api-design",
2154
+ "observability",
2155
+ "testing",
2156
+ "typescript-strict",
2157
+ "data-governance",
2158
+ "tech-stack"
2159
+ ),
2160
+ storage: skillSet(
2161
+ "api-design",
2162
+ "security-scanner",
2163
+ "testing",
2164
+ "observability",
2165
+ "node-modern",
2166
+ "tech-stack"
2167
+ ),
2168
+ "ml-engineer": skillSet(
2169
+ "observability",
2170
+ "typescript-strict",
2171
+ "testing",
2172
+ "security-scanner",
2173
+ "data-governance",
2174
+ "tech-stack"
2175
+ ),
2176
+ "data-governance": skillSet(
2177
+ "audit-log",
2178
+ "security-scanner",
2179
+ "observability",
2180
+ "testing",
2181
+ "output-standards",
2182
+ "tech-stack"
2183
+ ),
2184
+ // ── Wave 4: Agent platform and quality ───────────────────────────────
2185
+ "plugin-author": skillSet(
2186
+ "plugin-author",
2187
+ "node-modern",
2188
+ "typescript-strict",
2189
+ "testing",
2190
+ "security-scanner",
2191
+ "tech-stack"
2192
+ ),
2193
+ "tool-author": skillSet(
2194
+ "typescript-strict",
2195
+ "security-scanner",
2196
+ "testing",
2197
+ "output-standards",
2198
+ "tech-stack"
2199
+ ),
2200
+ "prompt-evaluator": skillSet(
2201
+ "prompt-engineering",
2202
+ "testing",
2203
+ "output-standards",
2204
+ "security-scanner",
2205
+ "tech-stack"
2206
+ ),
2207
+ "benchmark-engineer": skillSet("observability", "testing", "output-standards", "tech-stack"),
2208
+ "memory-curator": skillSet("mnemosyne", "audit-log", "testing", "output-standards", "tech-stack"),
2209
+ "fleet-coordinator": skillSet("multi-agent", "sdd", "output-standards", "testing", "tech-stack"),
2210
+ // ── Wave 2: Security, resilience, governance and agent platform ────────
2211
+ "threat-modeler": skillSet(
2212
+ "security-scanner",
2213
+ "api-design",
2214
+ "tech-stack",
2215
+ "audit-log",
2216
+ "testing"
2217
+ ),
2218
+ "secure-coding-coach": skillSet(
2219
+ "security-scanner",
2220
+ "testing",
2221
+ "typescript-strict",
2222
+ "api-design",
2223
+ "tech-stack"
2224
+ ),
2225
+ "resilience-engineer": skillSet(
2226
+ "observability",
2227
+ "testing",
2228
+ "typescript-strict",
2229
+ "api-design",
2230
+ "tech-stack"
2231
+ ),
2232
+ "chaos-engineer": skillSet(
2233
+ "observability",
2234
+ "testing",
2235
+ "security-scanner",
2236
+ "git-flow",
2237
+ "tech-stack"
2238
+ ),
2239
+ "compliance-auditor": skillSet(
2240
+ "audit-log",
2241
+ "security-scanner",
2242
+ "output-standards",
2243
+ "testing",
2244
+ "tech-stack"
2245
+ ),
2246
+ "privacy-engineer": skillSet(
2247
+ "security-scanner",
2248
+ "audit-log",
2249
+ "api-design",
2250
+ "data-governance",
2251
+ "tech-stack"
2252
+ )
2253
+ };
2254
+ var SHADOW_AGENT_SKILLS = skillSet(
2255
+ "wrongstack-mailbox",
2256
+ "multi-agent",
2257
+ "audit-log",
2258
+ "observability",
2259
+ "security-scanner"
2260
+ );
2261
+ var MAX_EAGER_ROSTER_SKILLS = 3;
2262
+ function assignSkillsToAgents(definitions) {
2263
+ return definitions.map((definition) => {
2264
+ const role = definition.config.role;
2265
+ if (!role || !Object.hasOwn(ROLE_SKILL_SETS, role)) {
2266
+ throw new Error(
2267
+ `Missing curated skill set for roster role: "${role ?? definition.config.name}"`
2268
+ );
2269
+ }
2270
+ const skills = ROLE_SKILL_SETS[role];
2271
+ return {
2272
+ ...definition,
2273
+ config: {
2274
+ ...definition.config,
2275
+ capabilities: inferRuntimeCapabilities(definition.config.tools ?? []),
2276
+ skillNames: skills.slice(0, MAX_EAGER_ROSTER_SKILLS),
2277
+ skillPool: [...skills]
2278
+ }
2279
+ };
2280
+ });
2281
+ }
2282
+
2283
+ // src/coordination/agents/project-agent-skill-layer.ts
2284
+ var SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
2285
+ function isProjectSkillName(name) {
2286
+ return SKILL_NAME_PATTERN.test(name);
2287
+ }
2288
+ function projectSkillsDir(role, projectRoot) {
2289
+ return path4.join(roleDir(role, projectRoot), "skills");
2290
+ }
2291
+ function listProjectSkillAugmentations(role, projectRoot) {
2292
+ try {
2293
+ return readdirSync(projectSkillsDir(role, projectRoot)).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).filter(isProjectSkillName).sort();
2294
+ } catch {
2295
+ return [];
2296
+ }
2297
+ }
2298
+
2299
+ // src/types/blocks.ts
2300
+ function isTextBlock(b) {
2301
+ return b.type === "text";
2302
+ }
2303
+
1834
2304
  // src/coordination/agents/project-agent-identity.ts
1835
2305
  function loadProjectAgentIdentity(role, projectRoot) {
1836
- const identityPath = path4.join(roleDir(role, projectRoot), "identity.md");
2306
+ const identityPath = path5.join(roleDir(role, projectRoot), "identity.md");
1837
2307
  try {
1838
- return readFileSync4(identityPath, "utf8").trim();
2308
+ return readFileSync5(identityPath, "utf8").trim();
1839
2309
  } catch {
1840
2310
  return "";
1841
2311
  }
1842
2312
  }
1843
2313
  function loadProjectAgentLearned(role, projectRoot) {
1844
- const learnedPath = path4.join(roleDir(role, projectRoot), "learned.md");
2314
+ const learnedPath = path5.join(roleDir(role, projectRoot), "learned.md");
1845
2315
  try {
1846
- return readFileSync4(learnedPath, "utf8").trim();
2316
+ return readFileSync5(learnedPath, "utf8").trim();
1847
2317
  } catch {
1848
2318
  return "";
1849
2319
  }
1850
2320
  }
1851
2321
  function loadRoleKnowledgeManifest(role, projectRoot) {
1852
- const projectPath = path4.join(roleDir(role, projectRoot), "knowledge.json");
2322
+ const projectPath = path5.join(roleDir(role, projectRoot), "knowledge.json");
1853
2323
  try {
1854
- const raw = readFileSync4(projectPath, "utf8");
2324
+ const raw = readFileSync5(projectPath, "utf8");
1855
2325
  return JSON.parse(raw);
1856
2326
  } catch {
1857
2327
  return BUILT_IN_KNOWLEDGE_MANIFESTS[role];
@@ -1879,31 +2349,35 @@ ${identity}`);
1879
2349
  let learnedLabel = "";
1880
2350
  if (learningPolicy.enabled) {
1881
2351
  const consolidated = loadProjectAgentConsolidated(role, projectRoot);
2352
+ const rawLearned = loadProjectAgentLearned(role, projectRoot);
1882
2353
  if (consolidated) {
1883
2354
  const meta = loadConsolidationMetadata(role, projectRoot);
1884
- const rawLearned = loadProjectAgentLearned(role, projectRoot);
1885
- const rawEntries = splitLearnedEntries(rawLearned);
2355
+ const rawEntries = parseStructuredLearnedEntriesFromContent(
2356
+ rawLearned,
2357
+ splitLearnedEntries(rawLearned)
2358
+ );
1886
2359
  const rawBytes = Buffer.byteLength(rawLearned, "utf8");
1887
- const stale = meta === void 0 || rawEntries.length > meta.sourceEntryCount || rawBytes > meta.sourceBytes;
1888
- if (stale) {
1889
- if (meta !== void 0 && meta.sourceEntryCount < rawEntries.length) {
1890
- const deltaEntries = rawEntries.slice(meta.sourceEntryCount);
1891
- learnedContent = `${consolidated}
2360
+ const freshEntries = meta === void 0 ? [] : rawEntries.filter((entry) => entry.capturedAt > meta.consolidatedAt);
2361
+ const stale = meta === void 0 || freshEntries.length > 0 || rawEntries.length > meta.sourceEntryCount || rawBytes > meta.sourceBytes;
2362
+ if (!stale) {
2363
+ learnedContent = consolidated;
2364
+ learnedLabel = "Consolidated knowledge for this project";
2365
+ } else if (freshEntries.length > 0) {
2366
+ const delta = freshEntries.map((entry) => `- **${entry.what}**`).join("\n");
2367
+ learnedContent = `${consolidated}
1892
2368
 
1893
2369
  ---
1894
2370
 
1895
2371
  ## Recently captured (pending next optimization)
1896
2372
 
1897
- ${deltaEntries.join("\n\n---\n\n")}`;
1898
- } else {
1899
- learnedContent = rawLearned;
1900
- }
2373
+ ${delta}`;
2374
+ learnedLabel = "Consolidated knowledge for this project";
1901
2375
  } else {
1902
- learnedContent = consolidated;
2376
+ learnedContent = rawLearned;
2377
+ learnedLabel = "Learned instructions for this project (structured: what / why / how)";
1903
2378
  }
1904
- learnedLabel = "Consolidated knowledge for this project";
1905
2379
  } else {
1906
- learnedContent = loadProjectAgentLearned(role, projectRoot);
2380
+ learnedContent = rawLearned;
1907
2381
  learnedLabel = "Learned instructions for this project (structured: what / why / how)";
1908
2382
  }
1909
2383
  }
@@ -1918,7 +2392,7 @@ ${learnedContent}`);
1918
2392
  }
1919
2393
  }
1920
2394
  const effectiveProjectRoot = projectRoot || process.cwd();
1921
- const learnedFilePath = path4.join(
2395
+ const learnedFilePath = path5.join(
1922
2396
  effectiveProjectRoot,
1923
2397
  ".wrongstack",
1924
2398
  "agents",
@@ -1945,6 +2419,10 @@ The file below stores **learning data for this project's "${role}" agent** \u201
1945
2419
  - **Self-contained** \u2014 understandable without the surrounding session context.
1946
2420
  - **Front-load concrete anchors** \u2014 commands in backticks, package names like \`@wrongstack/core\`, file paths like \`packages/core/src/.../foo.ts\`. The structured-list renderer extracts these as the "how" for the entry.
1947
2421
 
2422
+ **Your directives are scored against real outcomes.** After every task the runtime checks which stored directives were actually exercised \u2014 it matches their anchors against the report \u2014 and folds that task's success or failure into each one's record. A directive that keeps correlating with success outlives newer arrivals and survives rewording; one that has been exercised repeatedly and kept correlating with failure is retired and stops being injected. Two consequences for how you write them: anchors are what make a directive *measurable*, not just runnable, so an anchorless directive can never earn a record; and a directive you are unsure about costs nothing to write, because the loop will find out.
2423
+
2424
+ **Tag the skill you are developing.** When a directive refines one of your skills, mark it: \`## LEARNED [skill: testing]\`. Tagged directives are distilled into that skill's project addendum, so the lesson arrives as part of the skill itself on every future run instead of as a loose fact. Untagged directives are routed automatically when the wording makes the target obvious, and stay role-level otherwise.
2425
+
1948
2426
  **Bad** (session log \u2014 rejected at capture time):
1949
2427
 
1950
2428
  \`\`\`
@@ -1963,15 +2441,40 @@ Capture only durable, cross-session knowledge \u2014 never ephemeral task detail
1963
2441
  );
1964
2442
  }
1965
2443
  const knowledge = loadRoleKnowledgeManifest(role, projectRoot);
1966
- if (knowledge && knowledge.checklist.length > 0) {
1967
- const checklist = knowledge.checklist.map((item) => `- ${item}`).join("\n");
2444
+ if (knowledge) {
2445
+ const knowledgeSections = [];
2446
+ if (knowledge.checklist.length > 0) {
2447
+ knowledgeSections.push(
2448
+ `Verify these before answering:
2449
+ ${knowledge.checklist.map((item) => `- ${item}`).join("\n")}`
2450
+ );
2451
+ }
2452
+ const liveQueries = Object.entries(knowledge.liveQueries ?? {});
2453
+ if (liveQueries.length > 0) {
2454
+ const threshold = Number.isFinite(knowledge.verifyThreshold) ? knowledge.verifyThreshold : 0.5;
2455
+ knowledgeSections.push(
2456
+ `When your confidence in any of the following is below ${threshold}, fetch the live value rather than recalling it:
2457
+ ` + liveQueries.map(
2458
+ ([topic, query]) => `- **${topic}** \u2014 ${query.description}: \`${query.registry}\` \u2192 field \`${query.key}\``
2459
+ ).join("\n")
2460
+ );
2461
+ }
2462
+ if (knowledgeSections.length > 0) {
2463
+ parts.push(`
2464
+
2465
+ ## Current knowledge requirements
2466
+
2467
+ ${knowledgeSections.join("\n\n")}`);
2468
+ }
2469
+ }
2470
+ const developedSkills = listProjectSkillAugmentations(role, projectRoot);
2471
+ if (developedSkills.length > 0) {
1968
2472
  parts.push(
1969
2473
  `
1970
2474
 
1971
- ## Current knowledge requirements
2475
+ ## Project-developed skills
1972
2476
 
1973
- Verify these before answering:
1974
- ${checklist}`
2477
+ These skills have been extended with practice learned in this project: ${developedSkills.map((skill) => `\`${skill}\``).join(", ")}. Their project addendum is attached to the skill body \u2014 follow it over the generic method when the two differ.`
1975
2478
  );
1976
2479
  }
1977
2480
  const additions = parts.slice(1).join("\n\n").trim();
@@ -2060,38 +2563,38 @@ function loadTechVersionPolicy() {
2060
2563
  return techVersionPolicyBody;
2061
2564
  }
2062
2565
  function policyCandidateDirs() {
2063
- const here = path5.dirname(fileURLToPath(import.meta.url));
2566
+ const here = path6.dirname(fileURLToPath(import.meta.url));
2064
2567
  const profileInstructions = resolveWstackPaths({ projectRoot: process.cwd() }).globalInstructions;
2065
2568
  return [
2066
- path5.resolve(here, "../../../../instructions/agents"),
2067
- path5.resolve(here, "../../../instructions/agents"),
2068
- path5.resolve(here, "../../instructions/agents"),
2069
- path5.resolve(here, "../instructions/agents"),
2070
- path5.resolve(here, "instructions/agents"),
2071
- path5.join(profileInstructions, "agents"),
2569
+ path6.resolve(here, "../../../../instructions/agents"),
2570
+ path6.resolve(here, "../../../instructions/agents"),
2571
+ path6.resolve(here, "../../instructions/agents"),
2572
+ path6.resolve(here, "../instructions/agents"),
2573
+ path6.resolve(here, "instructions/agents"),
2574
+ path6.join(profileInstructions, "agents"),
2072
2575
  profileInstructions
2073
2576
  ].filter((dir, index, all) => all.indexOf(dir) === index);
2074
2577
  }
2075
2578
  function policyBody() {
2076
- const here = path5.dirname(fileURLToPath(import.meta.url));
2579
+ const here = path6.dirname(fileURLToPath(import.meta.url));
2077
2580
  const roots = [
2078
- path5.resolve(here, "_policy/tech-version.md"),
2079
- path5.resolve(here, "../_policy/tech-version.md"),
2080
- path5.resolve(here, "../../../instructions/agents/_policy/tech-version.md"),
2081
- path5.resolve(here, "../../instructions/agents/_policy/tech-version.md"),
2082
- path5.resolve(here, "../instructions/agents/_policy/tech-version.md"),
2083
- path5.resolve(here, "../../../../instructions/agents/_policy/tech-version.md"),
2084
- path5.resolve(here, "instructions/agents/_policy/tech-version.md")
2581
+ path6.resolve(here, "_policy/tech-version.md"),
2582
+ path6.resolve(here, "../_policy/tech-version.md"),
2583
+ path6.resolve(here, "../../../instructions/agents/_policy/tech-version.md"),
2584
+ path6.resolve(here, "../../instructions/agents/_policy/tech-version.md"),
2585
+ path6.resolve(here, "../instructions/agents/_policy/tech-version.md"),
2586
+ path6.resolve(here, "../../../../instructions/agents/_policy/tech-version.md"),
2587
+ path6.resolve(here, "instructions/agents/_policy/tech-version.md")
2085
2588
  ];
2086
2589
  for (const file of roots) {
2087
2590
  try {
2088
- return readFileSync5(file, "utf8").trim();
2591
+ return readFileSync6(file, "utf8").trim();
2089
2592
  } catch {
2090
2593
  }
2091
2594
  }
2092
2595
  for (const dir of policyCandidateDirs()) {
2093
2596
  try {
2094
- return readFileSync5(path5.join(dir, "_policy", "tech-version.md"), "utf8").trim();
2597
+ return readFileSync6(path6.join(dir, "_policy", "tech-version.md"), "utf8").trim();
2095
2598
  } catch {
2096
2599
  }
2097
2600
  }
@@ -2102,14 +2605,15 @@ function agentPrompt(id) {
2102
2605
  const envDir = process.env["WRONGSTACK_AGENT_INSTRUCTIONS_DIR"] ?? "";
2103
2606
  const policyOn = process.env["WRONGSTACK_AGENT_POLICY"] === "on";
2104
2607
  const cacheBypass = globalThis.__WS_DISABLE_PROMPT_CACHE__ === true;
2105
- const cacheKey = `${envDir}\0${policyOn ? 1 : 0}\0${id}`;
2608
+ const promptProjectRoot = process.env["WRONGSTACK_PROJECT_ROOT"] || process.cwd();
2609
+ const cacheKey = `${envDir}\0${policyOn ? 1 : 0}\0${promptProjectRoot}\0${id}`;
2106
2610
  const cached = cacheBypass ? void 0 : promptCache.get(cacheKey);
2107
2611
  if (cached !== void 0) return cached;
2108
2612
  const fileName = `${id}.md`;
2109
2613
  let resolved = "";
2110
2614
  for (const dir of agentPromptDirCandidates(envDir)) {
2111
2615
  try {
2112
- resolved = readFileSync5(path5.join(dir, fileName), "utf8").trim();
2616
+ resolved = readFileSync6(path6.join(dir, fileName), "utf8").trim();
2113
2617
  break;
2114
2618
  } catch {
2115
2619
  }
@@ -2126,8 +2630,7 @@ function agentPrompt(id) {
2126
2630
  ${policy}`;
2127
2631
  }
2128
2632
  }
2129
- const projectRoot = process.env["WRONGSTACK_PROJECT_ROOT"] || process.cwd();
2130
- resolved = buildProjectContextualizedPrompt(resolved, id, projectRoot);
2633
+ resolved = buildProjectContextualizedPrompt(resolved, id, promptProjectRoot);
2131
2634
  if (!cacheBypass) promptCache.set(cacheKey, resolved);
2132
2635
  return resolved;
2133
2636
  }
@@ -2136,16 +2639,16 @@ function agentPromptDirCandidates(envDir) {
2136
2639
  const candKey = `${envDir}\0${profileInstructions}`;
2137
2640
  const cached = candidateCache.get(candKey);
2138
2641
  if (cached !== void 0) return cached;
2139
- const here = path5.dirname(fileURLToPath(import.meta.url));
2642
+ const here = path6.dirname(fileURLToPath(import.meta.url));
2140
2643
  const explicitDir = envDir || void 0;
2141
2644
  const candidates = [
2142
- ...explicitDir ? [path5.resolve(explicitDir)] : [],
2143
- path5.join(profileInstructions, "agents"),
2144
- path5.resolve(here, "../../../../instructions/agents"),
2145
- path5.resolve(here, "../../../instructions/agents"),
2146
- path5.resolve(here, "../../instructions/agents"),
2147
- path5.resolve(here, "../instructions/agents"),
2148
- path5.resolve(here, "instructions/agents")
2645
+ ...explicitDir ? [path6.resolve(explicitDir)] : [],
2646
+ path6.join(profileInstructions, "agents"),
2647
+ path6.resolve(here, "../../../../instructions/agents"),
2648
+ path6.resolve(here, "../../../instructions/agents"),
2649
+ path6.resolve(here, "../../instructions/agents"),
2650
+ path6.resolve(here, "../instructions/agents"),
2651
+ path6.resolve(here, "instructions/agents")
2149
2652
  ];
2150
2653
  const ordered = candidates.sort((a, b) => Number(!isDirectory(a)) - Number(!isDirectory(b)));
2151
2654
  candidateCache.set(candKey, ordered);
@@ -2202,8 +2705,16 @@ var TOOLS = {
2202
2705
  "test",
2203
2706
  "mailbox"
2204
2707
  ],
2205
- /** Version control. */
2206
- vcs: ["read", "grep", "glob", "git", "diff"],
2708
+ /**
2709
+ * Version control.
2710
+ *
2711
+ * `mailbox` is in every preset on purpose: a subagent that hits a wall must
2712
+ * be able to say so. This was the one preset without it, which left the `git`
2713
+ * and `release` roles able to fail but not to ask — the two roles whose work
2714
+ * most often needs a decision from the leader (force-push, tag collision,
2715
+ * dirty tree) and least often has a safe default.
2716
+ */
2717
+ vcs: ["read", "grep", "glob", "git", "diff", "mailbox"],
2207
2718
  /** Dependency management + CVE audit. */
2208
2719
  deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
2209
2720
  /** Documentation authoring. */
@@ -4641,270 +5152,6 @@ var WAVE4_ROLE_METAS = {
4641
5152
  };
4642
5153
  var WAVE4_ROLES = new Set(Object.keys(WAVE4_ROLE_METAS));
4643
5154
 
4644
- // src/coordination/agents/role-skills.ts
4645
- var skillSet = (...names) => names;
4646
- var ROLE_SKILL_SETS = {
4647
- explore: skillSet("research-web", "node-modern", "typescript-strict"),
4648
- search: skillSet("bug-hunter", "typescript-strict", "research-web"),
4649
- research: skillSet("research-web", "tech-stack", "security-scanner", "api-design"),
4650
- analyst: skillSet("sdd", "api-design", "testing", "security-scanner"),
4651
- planner: skillSet("sdd", "multi-agent", "refactor-planner", "testing"),
4652
- architect: skillSet("sdd", "api-design", "refactor-planner", "security-scanner", "observability"),
4653
- critic: skillSet("chimera", "bug-hunter", "security-scanner", "refactor-planner"),
4654
- "refactor-planner": skillSet(
4655
- "refactor-planner",
4656
- "sdd",
4657
- "typescript-strict",
4658
- "testing",
4659
- "bug-hunter"
4660
- ),
4661
- executor: skillSet("sdd", "typescript-strict", "node-modern", "testing", "git-flow"),
4662
- refactor: skillSet("refactor-planner", "typescript-strict", "testing", "bug-hunter"),
4663
- simplifier: skillSet("bug-hunter", "typescript-strict", "testing", "refactor-planner"),
4664
- migration: skillSet("tech-stack", "node-modern", "typescript-strict", "testing", "git-flow"),
4665
- vision: skillSet("react-modern", "testing", "typescript-strict", "security-scanner"),
4666
- debugger: skillSet("bug-hunter", "testing", "typescript-strict", "observability", "audit-log"),
4667
- tracer: skillSet("observability", "audit-log", "bug-hunter", "testing"),
4668
- verifier: skillSet("testing", "typescript-strict", "security-scanner", "chimera"),
4669
- test: skillSet("testing", "typescript-strict", "bug-hunter", "security-scanner"),
4670
- e2e: skillSet("testing", "react-modern", "api-design", "security-scanner"),
4671
- browser: skillSet("testing", "react-modern", "security-scanner", "research-web"),
4672
- performance: skillSet("observability", "testing", "node-modern", "audit-log"),
4673
- chaos: skillSet("testing", "observability", "security-scanner", "audit-log"),
4674
- "security-scanner": skillSet("security-scanner", "bug-hunter", "api-design", "tech-stack"),
4675
- "bug-hunter": skillSet("bug-hunter", "typescript-strict", "testing", "security-scanner"),
4676
- "audit-log": skillSet("audit-log", "observability", "bug-hunter", "output-standards"),
4677
- reviewer: skillSet("chimera", "bug-hunter", "security-scanner", "testing"),
4678
- "code-reviewer": skillSet("chimera", "bug-hunter", "typescript-strict", "testing"),
4679
- "security-reviewer": skillSet("security-scanner", "chimera", "api-design", "tech-stack"),
4680
- accessibility: skillSet("react-modern", "testing", "chimera", "security-scanner"),
4681
- compliance: skillSet("security-scanner", "audit-log", "tech-stack", "chimera"),
4682
- database: skillSet("api-design", "testing", "security-scanner", "observability"),
4683
- api: skillSet("api-design", "typescript-strict", "node-modern", "testing", "security-scanner"),
4684
- auth: skillSet("security-scanner", "api-design", "testing", "typescript-strict", "audit-log"),
4685
- data: skillSet("typescript-strict", "testing", "observability", "security-scanner"),
4686
- frontend: skillSet("react-modern", "typescript-strict", "testing", "security-scanner"),
4687
- backend: skillSet("node-modern", "typescript-strict", "api-design", "testing", "observability"),
4688
- designer: skillSet("react-modern", "prompt-engineering", "research-web", "output-standards"),
4689
- ios: skillSet("sdd", "testing", "security-scanner", "research-web"),
4690
- document: skillSet("output-standards", "research-web", "api-design", "prompt-engineering"),
4691
- uml: skillSet("sdd", "refactor-planner", "api-design", "output-standards"),
4692
- i18n: skillSet("react-modern", "testing", "typescript-strict", "output-standards"),
4693
- prompt: skillSet("prompt-engineering", "skill-creator", "output-standards", "testing"),
4694
- git: skillSet("git-flow", "chimera", "testing", "security-scanner"),
4695
- release: skillSet("git-flow", "tech-stack", "chimera", "security-scanner"),
4696
- devops: skillSet("docker-deploy", "observability", "security-scanner", "git-flow", "tech-stack"),
4697
- observability: skillSet("observability", "node-modern", "testing", "audit-log"),
4698
- dependency: skillSet("tech-stack", "security-scanner", "node-modern", "git-flow"),
4699
- "skill-manage": skillSet(
4700
- "skill-creator",
4701
- "prompt-engineering",
4702
- "security-scanner",
4703
- "output-standards"
4704
- ),
4705
- "self-improving": skillSet(
4706
- "audit-log",
4707
- "mnemosyne",
4708
- "prompt-engineering",
4709
- "bug-hunter",
4710
- "observability"
4711
- ),
4712
- context: skillSet("mnemosyne", "output-standards", "prompt-engineering", "audit-log"),
4713
- cost: skillSet("audit-log", "tech-stack", "observability", "research-web"),
4714
- "tech-stack": skillSet("tech-stack", "research-web", "security-scanner", "node-modern"),
4715
- // ── Wave 1: Platform and systems engineering ──────────────────────────
4716
- android: skillSet(
4717
- "react-modern",
4718
- "typescript-strict",
4719
- "testing",
4720
- "security-scanner",
4721
- "tech-stack"
4722
- ),
4723
- desktop: skillSet(
4724
- "node-modern",
4725
- "typescript-strict",
4726
- "testing",
4727
- "security-scanner",
4728
- "tech-stack"
4729
- ),
4730
- realtime: skillSet(
4731
- "api-design",
4732
- "observability",
4733
- "node-modern",
4734
- "typescript-strict",
4735
- "testing",
4736
- "tech-stack"
4737
- ),
4738
- "distributed-systems": skillSet(
4739
- "api-design",
4740
- "observability",
4741
- "typescript-strict",
4742
- "testing",
4743
- "security-scanner",
4744
- "tech-stack"
4745
- ),
4746
- concurrency: skillSet(
4747
- "bug-hunter",
4748
- "typescript-strict",
4749
- "testing",
4750
- "observability",
4751
- "node-modern",
4752
- "tech-stack"
4753
- ),
4754
- "platform-engineer": skillSet(
4755
- "node-modern",
4756
- "typescript-strict",
4757
- "git-flow",
4758
- "docker-deploy",
4759
- "tech-stack"
4760
- ),
4761
- // ── Wave 3: Product and data specialists ──────────────────────────────
4762
- payments: skillSet(
4763
- "api-design",
4764
- "security-scanner",
4765
- "audit-log",
4766
- "testing",
4767
- "typescript-strict",
4768
- "tech-stack"
4769
- ),
4770
- messaging: skillSet(
4771
- "api-design",
4772
- "observability",
4773
- "node-modern",
4774
- "testing",
4775
- "security-scanner",
4776
- "tech-stack"
4777
- ),
4778
- "search-relevance": skillSet(
4779
- "api-design",
4780
- "observability",
4781
- "testing",
4782
- "typescript-strict",
4783
- "data-governance",
4784
- "tech-stack"
4785
- ),
4786
- storage: skillSet(
4787
- "api-design",
4788
- "security-scanner",
4789
- "testing",
4790
- "observability",
4791
- "node-modern",
4792
- "tech-stack"
4793
- ),
4794
- "ml-engineer": skillSet(
4795
- "observability",
4796
- "typescript-strict",
4797
- "testing",
4798
- "security-scanner",
4799
- "data-governance",
4800
- "tech-stack"
4801
- ),
4802
- "data-governance": skillSet(
4803
- "audit-log",
4804
- "security-scanner",
4805
- "observability",
4806
- "testing",
4807
- "output-standards",
4808
- "tech-stack"
4809
- ),
4810
- // ── Wave 4: Agent platform and quality ───────────────────────────────
4811
- "plugin-author": skillSet(
4812
- "plugin-author",
4813
- "node-modern",
4814
- "typescript-strict",
4815
- "testing",
4816
- "security-scanner",
4817
- "tech-stack"
4818
- ),
4819
- "tool-author": skillSet(
4820
- "typescript-strict",
4821
- "security-scanner",
4822
- "testing",
4823
- "output-standards",
4824
- "tech-stack"
4825
- ),
4826
- "prompt-evaluator": skillSet(
4827
- "prompt-engineering",
4828
- "testing",
4829
- "output-standards",
4830
- "security-scanner",
4831
- "tech-stack"
4832
- ),
4833
- "benchmark-engineer": skillSet("observability", "testing", "output-standards", "tech-stack"),
4834
- "memory-curator": skillSet("mnemosyne", "audit-log", "testing", "output-standards", "tech-stack"),
4835
- "fleet-coordinator": skillSet("multi-agent", "sdd", "output-standards", "testing", "tech-stack"),
4836
- // ── Wave 2: Security, resilience, governance and agent platform ────────
4837
- "threat-modeler": skillSet(
4838
- "security-scanner",
4839
- "api-design",
4840
- "tech-stack",
4841
- "audit-log",
4842
- "testing"
4843
- ),
4844
- "secure-coding-coach": skillSet(
4845
- "security-scanner",
4846
- "testing",
4847
- "typescript-strict",
4848
- "api-design",
4849
- "tech-stack"
4850
- ),
4851
- "resilience-engineer": skillSet(
4852
- "observability",
4853
- "testing",
4854
- "typescript-strict",
4855
- "api-design",
4856
- "tech-stack"
4857
- ),
4858
- "chaos-engineer": skillSet(
4859
- "observability",
4860
- "testing",
4861
- "security-scanner",
4862
- "git-flow",
4863
- "tech-stack"
4864
- ),
4865
- "compliance-auditor": skillSet(
4866
- "audit-log",
4867
- "security-scanner",
4868
- "output-standards",
4869
- "testing",
4870
- "tech-stack"
4871
- ),
4872
- "privacy-engineer": skillSet(
4873
- "security-scanner",
4874
- "audit-log",
4875
- "api-design",
4876
- "data-governance",
4877
- "tech-stack"
4878
- )
4879
- };
4880
- var SHADOW_AGENT_SKILLS = skillSet(
4881
- "wrongstack-mailbox",
4882
- "multi-agent",
4883
- "audit-log",
4884
- "observability",
4885
- "security-scanner"
4886
- );
4887
- var MAX_EAGER_ROSTER_SKILLS = 3;
4888
- function assignSkillsToAgents(definitions) {
4889
- return definitions.map((definition) => {
4890
- const role = definition.config.role;
4891
- if (!role || !Object.hasOwn(ROLE_SKILL_SETS, role)) {
4892
- throw new Error(
4893
- `Missing curated skill set for roster role: "${role ?? definition.config.name}"`
4894
- );
4895
- }
4896
- const skills = ROLE_SKILL_SETS[role];
4897
- return {
4898
- ...definition,
4899
- config: {
4900
- ...definition.config,
4901
- capabilities: inferRuntimeCapabilities(definition.config.tools ?? []),
4902
- skillNames: skills.slice(0, MAX_EAGER_ROSTER_SKILLS)
4903
- }
4904
- };
4905
- });
4906
- }
4907
-
4908
5155
  // src/coordination/agents/index.ts
4909
5156
  var ALL_AGENT_DEFINITIONS = assignSkillsToAgents([
4910
5157
  ...DISCOVERY_AGENTS,
@@ -5020,7 +5267,7 @@ function attachAutoExtend(events, policy = {}) {
5020
5267
  // src/coordination/delegate-tool.ts
5021
5268
  import { randomUUID as randomUUID2 } from "node:crypto";
5022
5269
  import * as fsp from "node:fs/promises";
5023
- import * as path6 from "node:path";
5270
+ import * as path7 from "node:path";
5024
5271
 
5025
5272
  // src/utils/safe-json.ts
5026
5273
  function safeParse(input, maxBytes = 5e6) {
@@ -5078,6 +5325,14 @@ var SHADOW_AGENT = {
5078
5325
  };
5079
5326
  var CRITIC_AGENT = defineAgent("critic", "Critic");
5080
5327
  var GENERIC_AGENT = defineAgent("generic", "Generic Project Agent");
5328
+ function withDispatchMetadata(definition) {
5329
+ const summary = definition.capability?.summary?.trim();
5330
+ if (!summary) return definition.config;
5331
+ return {
5332
+ ...definition.config,
5333
+ dispatch: { summary, keywords: [...definition.capability.keywords ?? []] }
5334
+ };
5335
+ }
5081
5336
  var FLEET_ROSTER = {
5082
5337
  "audit-log": AUDIT_LOG_AGENT,
5083
5338
  "bug-hunter": BUG_HUNTER_AGENT,
@@ -5087,7 +5342,7 @@ var FLEET_ROSTER = {
5087
5342
  generic: GENERIC_AGENT,
5088
5343
  "shadow-agent": SHADOW_AGENT,
5089
5344
  ...Object.fromEntries(
5090
- ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d.config])
5345
+ ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
5091
5346
  )
5092
5347
  };
5093
5348
  var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -5683,13 +5938,13 @@ async function readSubagentPartial(opts, subagentId) {
5683
5938
  if (!opts.sessionsRoot) return void 0;
5684
5939
  const candidates = [];
5685
5940
  if (opts.directorRunId) {
5686
- candidates.push(path6.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
5941
+ candidates.push(path7.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
5687
5942
  } else {
5688
5943
  try {
5689
5944
  const entries = await fsp.readdir(opts.sessionsRoot, { withFileTypes: true });
5690
5945
  for (const entry of entries) {
5691
5946
  if (entry.isDirectory()) {
5692
- candidates.push(path6.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
5947
+ candidates.push(path7.join(opts.sessionsRoot, entry.name, `${subagentId}.jsonl`));
5693
5948
  }
5694
5949
  }
5695
5950
  } catch {
@@ -6116,7 +6371,7 @@ var DirectorStateCheckpoint = class {
6116
6371
 
6117
6372
  // src/coordination/checkpoint-wiring.ts
6118
6373
  import * as fsp3 from "node:fs/promises";
6119
- import * as path7 from "node:path";
6374
+ import * as path8 from "node:path";
6120
6375
  async function appendSessionEvent(host, event) {
6121
6376
  if (!host.sessionWriter) return;
6122
6377
  try {
@@ -6164,7 +6419,7 @@ async function writeManifest(host) {
6164
6419
  }),
6165
6420
  usage: host.usage.snapshot()
6166
6421
  };
6167
- await fsp3.mkdir(path7.dirname(host.manifestPath), { recursive: true });
6422
+ await fsp3.mkdir(path8.dirname(host.manifestPath), { recursive: true });
6168
6423
  await atomicWrite(host.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
6169
6424
  return host.manifestPath;
6170
6425
  }
@@ -7727,9 +7982,6 @@ import {
7727
7982
  updateTaskAssignment
7728
7983
  } from "@wrongstack/kanban";
7729
7984
 
7730
- // src/coordination/director-kanban-queue-helpers.ts
7731
- import { describeKanbanBoundary } from "@wrongstack/kanban";
7732
-
7733
7985
  // src/coordination/director-input-helpers.ts
7734
7986
  import { randomUUID as randomUUID5 } from "node:crypto";
7735
7987
  function stringArray2(value) {
@@ -7750,6 +8002,7 @@ function instantiateRosterConfig2(role, base) {
7750
8002
  }
7751
8003
 
7752
8004
  // src/coordination/director-kanban-queue-helpers.ts
8005
+ import { describeKanbanBoundary } from "@wrongstack/kanban";
7753
8006
  function normalizeKanbanQueueInput(input) {
7754
8007
  const raw = input ?? {};
7755
8008
  return {
@@ -7918,8 +8171,8 @@ function ensureKanbanTool(tools) {
7918
8171
  }
7919
8172
 
7920
8173
  // src/utils/instruction-file.ts
7921
- import { readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
7922
- import * as path8 from "node:path";
8174
+ import { readFileSync as readFileSync7, statSync as statSync3 } from "node:fs";
8175
+ import * as path9 from "node:path";
7923
8176
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7924
8177
  var textCache = /* @__PURE__ */ new Map();
7925
8178
  var rootCandidates;
@@ -7929,7 +8182,7 @@ function readBundledInstructionText(relativePath) {
7929
8182
  let resolved = "";
7930
8183
  for (const root of instructionRootCandidates()) {
7931
8184
  try {
7932
- resolved = readFileSync6(path8.join(root, relativePath), "utf8").trimEnd();
8185
+ resolved = readFileSync7(path9.join(root, relativePath), "utf8").trimEnd();
7933
8186
  break;
7934
8187
  } catch {
7935
8188
  }
@@ -7945,11 +8198,11 @@ function renderInstructionTemplate(template, values) {
7945
8198
  }
7946
8199
  function instructionRootCandidates() {
7947
8200
  if (rootCandidates !== void 0) return rootCandidates;
7948
- const here = path8.dirname(fileURLToPath2(import.meta.url));
8201
+ const here = path9.dirname(fileURLToPath2(import.meta.url));
7949
8202
  const candidates = [
7950
- path8.resolve(here, "../../instructions"),
7951
- path8.resolve(here, "../instructions"),
7952
- path8.resolve(here, "instructions")
8203
+ path9.resolve(here, "../../instructions"),
8204
+ path9.resolve(here, "../instructions"),
8205
+ path9.resolve(here, "instructions")
7953
8206
  ];
7954
8207
  rootCandidates = candidates.sort((a, b) => Number(!isDirectory2(a)) - Number(!isDirectory2(b)));
7955
8208
  return rootCandidates;
@@ -8084,8 +8337,458 @@ function makeLLMClassifier(complete) {
8084
8337
  };
8085
8338
  }
8086
8339
 
8087
- // src/coordination/director-quality-gate-tool.ts
8340
+ // src/coordination/director-basic-tools.ts
8088
8341
  import { randomUUID as randomUUID6 } from "node:crypto";
8342
+ function makeAssignTool(director) {
8343
+ const inputSchema = {
8344
+ type: "object",
8345
+ properties: {
8346
+ subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
8347
+ description: {
8348
+ type: "string",
8349
+ minLength: 1,
8350
+ description: "The task in natural language \u2014 what you want this subagent to do."
8351
+ },
8352
+ maxToolCalls: {
8353
+ type: "number",
8354
+ minimum: 1,
8355
+ description: "Optional per-task tool-call budget override."
8356
+ },
8357
+ timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
8358
+ },
8359
+ required: ["subagentId", "description"]
8360
+ };
8361
+ return {
8362
+ name: "assign_task",
8363
+ description: "Queue a task on a previously spawned subagent. NON-BLOCKING: returns a `taskId` IMMEDIATELY \u2014 the subagent processes the task on its next iteration with its own LLM budget. The `taskId` is the durable handle for retrieving the result later via `await_tasks`, `roll_up`, or `ask_result`. Many `assign_task` calls can be in flight in parallel against the same or different subagents. This is the primary tool for fan-out work; do NOT use `delegate` to spawn multiple investigations sequentially.",
8364
+ permission: "auto",
8365
+ mutating: false,
8366
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8367
+ inputSchema,
8368
+ async execute(input) {
8369
+ const i = input;
8370
+ const task = {
8371
+ id: randomUUID6(),
8372
+ description: i.description,
8373
+ subagentId: i.subagentId,
8374
+ maxToolCalls: i.maxToolCalls,
8375
+ timeoutMs: i.timeoutMs
8376
+ };
8377
+ const taskId = await director.assign(task);
8378
+ return { taskId, subagentId: i.subagentId };
8379
+ }
8380
+ };
8381
+ }
8382
+ function makeAwaitTasksTool(director) {
8383
+ return {
8384
+ name: "await_tasks",
8385
+ description: 'Block until one or more `taskId`s complete, then return their results. The subagents keep running in the background \u2014 only the leader\'s iteration pauses, which is the point: this is the correct tool to retrieve a result you started earlier with `assign_task`. mode:"all" (default) blocks until EVERY named task completes. mode:"any" returns as soon as AT LEAST ONE completes \u2014 use it for independent tasks so you can handle each finisher immediately (reassign work, spawn helpers) instead of idling on the slowest; call again with the returned `pending` ids to pick up the next finisher. The pattern "fan out via assign_task, then await_tasks({mode:\'any\'})" is the async replacement for serial `delegate` calls.',
8386
+ permission: "auto",
8387
+ mutating: false,
8388
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8389
+ inputSchema: {
8390
+ type: "object",
8391
+ properties: {
8392
+ taskIds: {
8393
+ type: "array",
8394
+ items: { type: "string" },
8395
+ description: "One or more task ids returned by `assign_task`."
8396
+ },
8397
+ mode: {
8398
+ type: "string",
8399
+ enum: ["all", "any"],
8400
+ description: '"all" (default): block until every task completes. "any": return on the first completion with the rest listed as pending.'
8401
+ },
8402
+ timeoutMs: {
8403
+ type: "number",
8404
+ minimum: 1,
8405
+ description: 'mode:"any" only \u2014 return {timedOut:true, completed:[]} if nothing completes within this window instead of blocking.'
8406
+ }
8407
+ },
8408
+ required: ["taskIds"]
8409
+ },
8410
+ async execute(input) {
8411
+ const i = input;
8412
+ if (i.mode === "any") {
8413
+ const r = await director.awaitTasksAny(
8414
+ i.taskIds,
8415
+ i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : void 0
8416
+ );
8417
+ return {
8418
+ mode: "any",
8419
+ completed: r.completed,
8420
+ pending: r.pending,
8421
+ ...r.timedOut ? { timedOut: true } : {},
8422
+ ...r.pending.length > 0 ? {
8423
+ hint: 'Handle the completed results now. Re-call await_tasks with the pending ids (mode:"any") for the next finisher \u2014 or assign new work to the now-idle subagent first.'
8424
+ } : {}
8425
+ };
8426
+ }
8427
+ const results = await director.awaitTasks(i.taskIds);
8428
+ return { results };
8429
+ }
8430
+ };
8431
+ }
8432
+ function makeAskTool(director) {
8433
+ return {
8434
+ name: "ask_subagent",
8435
+ description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
8436
+ permission: "auto",
8437
+ mutating: false,
8438
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8439
+ inputSchema: {
8440
+ type: "object",
8441
+ properties: {
8442
+ subagentId: {
8443
+ type: "string",
8444
+ minLength: 1,
8445
+ description: "Subagent to ask. Must be a previously spawned id."
8446
+ },
8447
+ question: { type: "string", minLength: 1, description: "The question or instruction." },
8448
+ timeoutMs: {
8449
+ type: "number",
8450
+ minimum: 1,
8451
+ description: "Optional timeout in ms (default 30s)."
8452
+ }
8453
+ },
8454
+ required: ["subagentId", "question"]
8455
+ },
8456
+ async execute(input) {
8457
+ const i = input;
8458
+ try {
8459
+ const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
8460
+ const stored = director.largeAnswerStore.storeAnswer(answer);
8461
+ if (stored.inline) {
8462
+ return { ok: true, answer: stored.summary };
8463
+ }
8464
+ return {
8465
+ ok: true,
8466
+ answer: stored.summary,
8467
+ _answerKey: stored.key,
8468
+ _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
8469
+ };
8470
+ } catch (err) {
8471
+ return { ok: false, error: toErrorMessage(err) };
8472
+ }
8473
+ }
8474
+ };
8475
+ }
8476
+ function makeAskResultTool(director) {
8477
+ return {
8478
+ name: "ask_result",
8479
+ description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
8480
+ permission: "auto",
8481
+ mutating: false,
8482
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8483
+ inputSchema: {
8484
+ type: "object",
8485
+ properties: {
8486
+ key: {
8487
+ type: "string",
8488
+ minLength: 1,
8489
+ description: "The `_answerKey` returned by `ask_subagent` for a large response."
8490
+ }
8491
+ },
8492
+ required: ["key"]
8493
+ },
8494
+ async execute(input) {
8495
+ const i = input;
8496
+ const value = director.largeAnswerStore.retrieveAnswer(i.key);
8497
+ if (value === void 0) {
8498
+ return {
8499
+ ok: false,
8500
+ error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.`
8501
+ };
8502
+ }
8503
+ return { ok: true, value };
8504
+ }
8505
+ };
8506
+ }
8507
+ function makeRollUpTool(director) {
8508
+ return {
8509
+ name: "roll_up",
8510
+ description: "Aggregate completed task results into a single formatted summary.",
8511
+ permission: "auto",
8512
+ mutating: false,
8513
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8514
+ inputSchema: {
8515
+ type: "object",
8516
+ properties: {
8517
+ taskIds: {
8518
+ type: "array",
8519
+ items: { type: "string" },
8520
+ description: "Completed task ids to aggregate."
8521
+ },
8522
+ style: {
8523
+ type: "string",
8524
+ enum: ["markdown", "json"],
8525
+ description: "Output flavor \u2014 markdown (default) or json."
8526
+ }
8527
+ },
8528
+ required: ["taskIds"]
8529
+ },
8530
+ async execute(input) {
8531
+ const i = input;
8532
+ const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
8533
+ return { summary, count: i.taskIds.length };
8534
+ }
8535
+ };
8536
+ }
8537
+ function makeTerminateTool(director) {
8538
+ return {
8539
+ name: "terminate_subagent",
8540
+ description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
8541
+ permission: "auto",
8542
+ mutating: true,
8543
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8544
+ inputSchema: {
8545
+ type: "object",
8546
+ properties: { subagentId: { type: "string", description: "Subagent to abort." } },
8547
+ required: ["subagentId"]
8548
+ },
8549
+ async execute(input) {
8550
+ const i = input;
8551
+ await director.terminate(i.subagentId);
8552
+ return { ok: true };
8553
+ }
8554
+ };
8555
+ }
8556
+ function makeTerminateAllTool(director) {
8557
+ return {
8558
+ name: "terminate_all",
8559
+ description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
8560
+ permission: "auto",
8561
+ mutating: true,
8562
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8563
+ inputSchema: { type: "object", properties: {}, required: [] },
8564
+ async execute() {
8565
+ await director.terminateAll();
8566
+ return {
8567
+ ok: true,
8568
+ message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.`
8569
+ };
8570
+ }
8571
+ };
8572
+ }
8573
+ function makeFleetTool(director) {
8574
+ return {
8575
+ name: "fleet",
8576
+ description: 'Fleet observation tool. Use `action` to select what you need: "status" \u2014 snapshot of all subagents + coordinator counts + pending tasks; "usage" \u2014 token + cost breakdown per subagent and totals; "health" \u2014 per-subagent budget pressure, last activity, and status; "session" \u2014 read a subagent\'s JSONL transcript (requires subagentId).',
8577
+ usageHint: 'action: "status" (default) | "usage" | "health" | "session".\nFor "session", pass subagentId (required) and optional tail (trailing JSONL lines).',
8578
+ permission: "auto",
8579
+ mutating: false,
8580
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8581
+ inputSchema: {
8582
+ type: "object",
8583
+ properties: {
8584
+ action: {
8585
+ type: "string",
8586
+ enum: ["status", "usage", "health", "session"],
8587
+ description: "Observation to retrieve (default: status)."
8588
+ },
8589
+ subagentId: {
8590
+ type: "string",
8591
+ description: 'Subagent id (required for action: "session").'
8592
+ },
8593
+ tail: {
8594
+ type: "number",
8595
+ description: 'Number of trailing JSONL lines (action: "session" only). Omit for the full transcript.'
8596
+ }
8597
+ }
8598
+ },
8599
+ async execute(input) {
8600
+ const i = input ?? {};
8601
+ const action = i.action ?? "status";
8602
+ switch (action) {
8603
+ case "status": {
8604
+ const base = director.status();
8605
+ const fm = director.fleetManager;
8606
+ const stats = fm?.getFleetStats();
8607
+ const fleetStatus = fm?.getFleetStatus();
8608
+ return {
8609
+ action: "status",
8610
+ subagents: base.subagents,
8611
+ coordinatorStats: stats ? {
8612
+ total: stats.total,
8613
+ running: stats.running,
8614
+ idle: stats.idle,
8615
+ stopped: stats.stopped
8616
+ } : void 0,
8617
+ pending: fleetStatus?.pending ?? [],
8618
+ usage: fm?.snapshot()
8619
+ };
8620
+ }
8621
+ case "usage": {
8622
+ return { action: "usage", ...director.snapshot() };
8623
+ }
8624
+ case "health": {
8625
+ const status = director.status();
8626
+ const snapshot = director.snapshot();
8627
+ const subagents = status.subagents ?? [];
8628
+ const perSubagent = snapshot.perSubagent ?? {};
8629
+ return {
8630
+ action: "health",
8631
+ subagents: subagents.map((s) => {
8632
+ const usage = perSubagent[s.id];
8633
+ return {
8634
+ id: s.id,
8635
+ status: s.status,
8636
+ lastEventAt: usage?.lastEventAt,
8637
+ budgetPressure: {
8638
+ iterations: usage?.iterations,
8639
+ toolCalls: usage?.toolCalls,
8640
+ costUsd: usage?.cost
8641
+ }
8642
+ };
8643
+ })
8644
+ };
8645
+ }
8646
+ case "session": {
8647
+ const subagentId = i.subagentId;
8648
+ if (!subagentId) {
8649
+ return {
8650
+ action: "session",
8651
+ error: 'fleet: subagentId is required for action: "session"'
8652
+ };
8653
+ }
8654
+ const result = await director.readSession(subagentId, i.tail);
8655
+ if (!result) {
8656
+ return {
8657
+ action: "session",
8658
+ error: `fleet: transcript unavailable for "${subagentId}". Is sessionsRoot configured?`
8659
+ };
8660
+ }
8661
+ return { action: "session", ...result };
8662
+ }
8663
+ default:
8664
+ return {
8665
+ error: `fleet: unknown action "${action}". Valid: status, usage, health, session.`
8666
+ };
8667
+ }
8668
+ }
8669
+ };
8670
+ }
8671
+
8672
+ // src/coordination/director-collab-tools.ts
8673
+ function makeCollabDebugTool(director) {
8674
+ return {
8675
+ name: "collab_debug",
8676
+ description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
8677
+ permission: "auto",
8678
+ mutating: false,
8679
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8680
+ inputSchema: {
8681
+ type: "object",
8682
+ properties: {
8683
+ targetPaths: {
8684
+ type: "array",
8685
+ items: { type: "string" },
8686
+ description: "File paths / glob patterns to scan for bugs."
8687
+ },
8688
+ timeoutMs: {
8689
+ type: "number",
8690
+ minimum: 1,
8691
+ description: "Timeout in ms. Default: 600000 (10 minutes)."
8692
+ },
8693
+ maxTargetFiles: {
8694
+ type: "number",
8695
+ minimum: 1,
8696
+ description: "Maximum number of files to include in the snapshot. If not set, the limit is computed dynamically from contextWindow or falls back to the default (30)."
8697
+ },
8698
+ contextWindow: {
8699
+ type: "number",
8700
+ minimum: 1,
8701
+ description: "Context window size (tokens) of the model. When provided and maxTargetFiles is not set, the file limit is computed dynamically as floor((contextWindow * 0.4) / 2000)."
8702
+ }
8703
+ },
8704
+ required: ["targetPaths"]
8705
+ },
8706
+ async execute(input) {
8707
+ const i = input;
8708
+ if (!i.targetPaths?.length) {
8709
+ return { error: "collab_debug: targetPaths is required and must be non-empty." };
8710
+ }
8711
+ const options = {
8712
+ targetPaths: i.targetPaths,
8713
+ timeoutMs: i.timeoutMs,
8714
+ maxTargetFiles: i.maxTargetFiles,
8715
+ contextWindow: i.contextWindow
8716
+ };
8717
+ try {
8718
+ const report = await director.spawnCollab(options);
8719
+ return {
8720
+ sessionId: report.sessionId,
8721
+ overallVerdict: report.overallVerdict,
8722
+ bugCount: report.bugs.length,
8723
+ planCount: report.refactorPlans.length,
8724
+ evaluationCount: report.evaluations.length,
8725
+ summary: report.summary,
8726
+ bugs: report.bugs,
8727
+ refactorPlans: report.refactorPlans,
8728
+ evaluations: report.evaluations
8729
+ };
8730
+ } catch (err) {
8731
+ return { error: "collab_debug failed: " + toErrorMessage(err) };
8732
+ }
8733
+ }
8734
+ };
8735
+ }
8736
+ function makeFleetEmitTool(director) {
8737
+ return {
8738
+ name: "fleet_emit",
8739
+ description: "Emit a structured event on the FleetBus. Known collaboration events are schema-validated and role-bound; custom event types remain extensible. Use it to stream findings, progress updates, or final results to other agents in real time.",
8740
+ permission: "auto",
8741
+ mutating: false,
8742
+ capabilities: [ToolCapabilities.COORDINATION_FLEET_EMIT],
8743
+ inputSchema: {
8744
+ type: "object",
8745
+ properties: {
8746
+ type: {
8747
+ type: "string",
8748
+ description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
8749
+ },
8750
+ payload: {
8751
+ type: "object",
8752
+ description: "Event payload. Structure depends on event type. Use null if no payload."
8753
+ }
8754
+ },
8755
+ required: ["type"]
8756
+ },
8757
+ async execute(input, ctx) {
8758
+ const i = input;
8759
+ const role = ctx.meta["agentRole"];
8760
+ const validationError = validateFleetEventEmission(i.type, i.payload ?? {}, role);
8761
+ if (validationError) return { ok: false, error: validationError };
8762
+ const callerId = ctx.agentId && ctx.agentId !== "unknown" ? ctx.agentId : director.id;
8763
+ const taskId = ctx.meta["subagentTaskId"];
8764
+ director.fleet.emit({
8765
+ subagentId: callerId,
8766
+ taskId,
8767
+ ts: Date.now(),
8768
+ type: i.type,
8769
+ payload: i.payload ?? {}
8770
+ });
8771
+ return { ok: true, event: i.type };
8772
+ }
8773
+ };
8774
+ }
8775
+ function makeWorkCompleteTool(director) {
8776
+ return {
8777
+ name: "work_complete",
8778
+ description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
8779
+ permission: "auto",
8780
+ mutating: false,
8781
+ capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8782
+ inputSchema: { type: "object", properties: {}, required: [] },
8783
+ async execute() {
8784
+ director.workComplete();
8785
+ return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
8786
+ }
8787
+ };
8788
+ }
8789
+
8790
+ // src/coordination/director-quality-gate-tool.ts
8791
+ import { randomUUID as randomUUID7 } from "node:crypto";
8089
8792
  function makeQualityGateTool(director, roster) {
8090
8793
  return {
8091
8794
  name: "quality_gate",
@@ -8182,7 +8885,7 @@ function makeQualityGateTool(director, roster) {
8182
8885
  makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
8183
8886
  );
8184
8887
  const taskId = await director.assign({
8185
- id: randomUUID6(),
8888
+ id: randomUUID7(),
8186
8889
  subagentId,
8187
8890
  description: buildVerifierTask(i, {
8188
8891
  attempt,
@@ -8200,7 +8903,7 @@ function makeQualityGateTool(director, roster) {
8200
8903
  makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
8201
8904
  );
8202
8905
  const taskId = await director.assign({
8203
- id: randomUUID6(),
8906
+ id: randomUUID7(),
8204
8907
  subagentId,
8205
8908
  description: buildReviewerTask(i, {
8206
8909
  attempt,
@@ -8228,7 +8931,7 @@ function makeQualityGateTool(director, roster) {
8228
8931
  };
8229
8932
  }
8230
8933
  const repairTaskId = await director.assign({
8231
- id: randomUUID6(),
8934
+ id: randomUUID7(),
8232
8935
  subagentId: i.repairSubagentId,
8233
8936
  description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
8234
8937
  timeoutMs: i.timeoutMs
@@ -8458,456 +9161,6 @@ function excerpt(text, max) {
8458
9161
  ...(truncated)`;
8459
9162
  }
8460
9163
 
8461
- // src/coordination/director-collab-tools.ts
8462
- function makeCollabDebugTool(director) {
8463
- return {
8464
- name: "collab_debug",
8465
- description: "Start a collaborative debugging session: BugHunter, RefactorPlanner, and Critic run in parallel on the same target files. BugHunter finds bugs and emits bug.found events. RefactorPlanner listens for bug.found and emits refactor.plan events. Critic evaluates both and emits critic.evaluation events. Returns a structured report with overall verdict (approve / needs_revision / reject).",
8466
- permission: "auto",
8467
- mutating: false,
8468
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8469
- inputSchema: {
8470
- type: "object",
8471
- properties: {
8472
- targetPaths: {
8473
- type: "array",
8474
- items: { type: "string" },
8475
- description: "File paths / glob patterns to scan for bugs."
8476
- },
8477
- timeoutMs: {
8478
- type: "number",
8479
- minimum: 1,
8480
- description: "Timeout in ms. Default: 600000 (10 minutes)."
8481
- },
8482
- maxTargetFiles: {
8483
- type: "number",
8484
- minimum: 1,
8485
- description: "Maximum number of files to include in the snapshot. If not set, the limit is computed dynamically from contextWindow or falls back to the default (30)."
8486
- },
8487
- contextWindow: {
8488
- type: "number",
8489
- minimum: 1,
8490
- description: "Context window size (tokens) of the model. When provided and maxTargetFiles is not set, the file limit is computed dynamically as floor((contextWindow * 0.4) / 2000)."
8491
- }
8492
- },
8493
- required: ["targetPaths"]
8494
- },
8495
- async execute(input) {
8496
- const i = input;
8497
- if (!i.targetPaths?.length) {
8498
- return { error: "collab_debug: targetPaths is required and must be non-empty." };
8499
- }
8500
- const options = {
8501
- targetPaths: i.targetPaths,
8502
- timeoutMs: i.timeoutMs,
8503
- maxTargetFiles: i.maxTargetFiles,
8504
- contextWindow: i.contextWindow
8505
- };
8506
- try {
8507
- const report = await director.spawnCollab(options);
8508
- return {
8509
- sessionId: report.sessionId,
8510
- overallVerdict: report.overallVerdict,
8511
- bugCount: report.bugs.length,
8512
- planCount: report.refactorPlans.length,
8513
- evaluationCount: report.evaluations.length,
8514
- summary: report.summary,
8515
- bugs: report.bugs,
8516
- refactorPlans: report.refactorPlans,
8517
- evaluations: report.evaluations
8518
- };
8519
- } catch (err) {
8520
- return { error: "collab_debug failed: " + toErrorMessage(err) };
8521
- }
8522
- }
8523
- };
8524
- }
8525
- function makeFleetEmitTool(director) {
8526
- return {
8527
- name: "fleet_emit",
8528
- description: "Emit a structured event on the FleetBus. Known collaboration events are schema-validated and role-bound; custom event types remain extensible. Use it to stream findings, progress updates, or final results to other agents in real time.",
8529
- permission: "auto",
8530
- mutating: false,
8531
- capabilities: [ToolCapabilities.COORDINATION_FLEET_EMIT],
8532
- inputSchema: {
8533
- type: "object",
8534
- properties: {
8535
- type: {
8536
- type: "string",
8537
- description: "Event type string (e.g. bug.found, refactor.plan, critic.evaluation, progress, result)."
8538
- },
8539
- payload: {
8540
- type: "object",
8541
- description: "Event payload. Structure depends on event type. Use null if no payload."
8542
- }
8543
- },
8544
- required: ["type"]
8545
- },
8546
- async execute(input, ctx) {
8547
- const i = input;
8548
- const role = ctx.meta["agentRole"];
8549
- const validationError = validateFleetEventEmission(i.type, i.payload ?? {}, role);
8550
- if (validationError) return { ok: false, error: validationError };
8551
- const callerId = ctx.agentId && ctx.agentId !== "unknown" ? ctx.agentId : director.id;
8552
- const taskId = ctx.meta["subagentTaskId"];
8553
- director.fleet.emit({
8554
- subagentId: callerId,
8555
- taskId,
8556
- ts: Date.now(),
8557
- type: i.type,
8558
- payload: i.payload ?? {}
8559
- });
8560
- return { ok: true, event: i.type };
8561
- }
8562
- };
8563
- }
8564
- function makeWorkCompleteTool(director) {
8565
- return {
8566
- name: "work_complete",
8567
- description: "Signal that the director is satisfied with the results and the fleet should wind down. After calling this, spawn_subagent will refuse with a budget error and assign_task will instantly complete any queued tasks as aborted. Running subagents finish naturally. Call terminate_subagent separately to stop specific subagents immediately.",
8568
- permission: "auto",
8569
- mutating: false,
8570
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8571
- inputSchema: { type: "object", properties: {}, required: [] },
8572
- async execute() {
8573
- director.workComplete();
8574
- return { ok: true, message: "Fleet wind-down signaled. No new spawns or task dispatches." };
8575
- }
8576
- };
8577
- }
8578
-
8579
- // src/coordination/director-basic-tools.ts
8580
- import { randomUUID as randomUUID7 } from "node:crypto";
8581
- function makeAssignTool(director) {
8582
- const inputSchema = {
8583
- type: "object",
8584
- properties: {
8585
- subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
8586
- description: {
8587
- type: "string",
8588
- minLength: 1,
8589
- description: "The task in natural language \u2014 what you want this subagent to do."
8590
- },
8591
- maxToolCalls: {
8592
- type: "number",
8593
- minimum: 1,
8594
- description: "Optional per-task tool-call budget override."
8595
- },
8596
- timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
8597
- },
8598
- required: ["subagentId", "description"]
8599
- };
8600
- return {
8601
- name: "assign_task",
8602
- description: "Queue a task on a previously spawned subagent. NON-BLOCKING: returns a `taskId` IMMEDIATELY \u2014 the subagent processes the task on its next iteration with its own LLM budget. The `taskId` is the durable handle for retrieving the result later via `await_tasks`, `roll_up`, or `ask_result`. Many `assign_task` calls can be in flight in parallel against the same or different subagents. This is the primary tool for fan-out work; do NOT use `delegate` to spawn multiple investigations sequentially.",
8603
- permission: "auto",
8604
- mutating: false,
8605
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8606
- inputSchema,
8607
- async execute(input) {
8608
- const i = input;
8609
- const task = {
8610
- id: randomUUID7(),
8611
- description: i.description,
8612
- subagentId: i.subagentId,
8613
- maxToolCalls: i.maxToolCalls,
8614
- timeoutMs: i.timeoutMs
8615
- };
8616
- const taskId = await director.assign(task);
8617
- return { taskId, subagentId: i.subagentId };
8618
- }
8619
- };
8620
- }
8621
- function makeAwaitTasksTool(director) {
8622
- return {
8623
- name: "await_tasks",
8624
- description: 'Block until one or more `taskId`s complete, then return their results. The subagents keep running in the background \u2014 only the leader\'s iteration pauses, which is the point: this is the correct tool to retrieve a result you started earlier with `assign_task`. mode:"all" (default) blocks until EVERY named task completes. mode:"any" returns as soon as AT LEAST ONE completes \u2014 use it for independent tasks so you can handle each finisher immediately (reassign work, spawn helpers) instead of idling on the slowest; call again with the returned `pending` ids to pick up the next finisher. The pattern "fan out via assign_task, then await_tasks({mode:\'any\'})" is the async replacement for serial `delegate` calls.',
8625
- permission: "auto",
8626
- mutating: false,
8627
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8628
- inputSchema: {
8629
- type: "object",
8630
- properties: {
8631
- taskIds: {
8632
- type: "array",
8633
- items: { type: "string" },
8634
- description: "One or more task ids returned by `assign_task`."
8635
- },
8636
- mode: {
8637
- type: "string",
8638
- enum: ["all", "any"],
8639
- description: '"all" (default): block until every task completes. "any": return on the first completion with the rest listed as pending.'
8640
- },
8641
- timeoutMs: {
8642
- type: "number",
8643
- minimum: 1,
8644
- description: 'mode:"any" only \u2014 return {timedOut:true, completed:[]} if nothing completes within this window instead of blocking.'
8645
- }
8646
- },
8647
- required: ["taskIds"]
8648
- },
8649
- async execute(input) {
8650
- const i = input;
8651
- if (i.mode === "any") {
8652
- const r = await director.awaitTasksAny(
8653
- i.taskIds,
8654
- i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : void 0
8655
- );
8656
- return {
8657
- mode: "any",
8658
- completed: r.completed,
8659
- pending: r.pending,
8660
- ...r.timedOut ? { timedOut: true } : {},
8661
- ...r.pending.length > 0 ? {
8662
- hint: 'Handle the completed results now. Re-call await_tasks with the pending ids (mode:"any") for the next finisher \u2014 or assign new work to the now-idle subagent first.'
8663
- } : {}
8664
- };
8665
- }
8666
- const results = await director.awaitTasks(i.taskIds);
8667
- return { results };
8668
- }
8669
- };
8670
- }
8671
- function makeAskTool(director) {
8672
- return {
8673
- name: "ask_subagent",
8674
- description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
8675
- permission: "auto",
8676
- mutating: false,
8677
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8678
- inputSchema: {
8679
- type: "object",
8680
- properties: {
8681
- subagentId: {
8682
- type: "string",
8683
- minLength: 1,
8684
- description: "Subagent to ask. Must be a previously spawned id."
8685
- },
8686
- question: { type: "string", minLength: 1, description: "The question or instruction." },
8687
- timeoutMs: {
8688
- type: "number",
8689
- minimum: 1,
8690
- description: "Optional timeout in ms (default 30s)."
8691
- }
8692
- },
8693
- required: ["subagentId", "question"]
8694
- },
8695
- async execute(input) {
8696
- const i = input;
8697
- try {
8698
- const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
8699
- const stored = director.largeAnswerStore.storeAnswer(answer);
8700
- if (stored.inline) {
8701
- return { ok: true, answer: stored.summary };
8702
- }
8703
- return {
8704
- ok: true,
8705
- answer: stored.summary,
8706
- _answerKey: stored.key,
8707
- _hint: "Response was large and stored. Use ask_result with the key to retrieve it."
8708
- };
8709
- } catch (err) {
8710
- return { ok: false, error: toErrorMessage(err) };
8711
- }
8712
- }
8713
- };
8714
- }
8715
- function makeAskResultTool(director) {
8716
- return {
8717
- name: "ask_result",
8718
- description: "Retrieve a large `ask_subagent` response that was stored out-of-context (>2K chars). Returns the full stored value.",
8719
- permission: "auto",
8720
- mutating: false,
8721
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8722
- inputSchema: {
8723
- type: "object",
8724
- properties: {
8725
- key: {
8726
- type: "string",
8727
- minLength: 1,
8728
- description: "The `_answerKey` returned by `ask_subagent` for a large response."
8729
- }
8730
- },
8731
- required: ["key"]
8732
- },
8733
- async execute(input) {
8734
- const i = input;
8735
- const value = director.largeAnswerStore.retrieveAnswer(i.key);
8736
- if (value === void 0) {
8737
- return {
8738
- ok: false,
8739
- error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.`
8740
- };
8741
- }
8742
- return { ok: true, value };
8743
- }
8744
- };
8745
- }
8746
- function makeRollUpTool(director) {
8747
- return {
8748
- name: "roll_up",
8749
- description: "Aggregate completed task results into a single formatted summary.",
8750
- permission: "auto",
8751
- mutating: false,
8752
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8753
- inputSchema: {
8754
- type: "object",
8755
- properties: {
8756
- taskIds: {
8757
- type: "array",
8758
- items: { type: "string" },
8759
- description: "Completed task ids to aggregate."
8760
- },
8761
- style: {
8762
- type: "string",
8763
- enum: ["markdown", "json"],
8764
- description: "Output flavor \u2014 markdown (default) or json."
8765
- }
8766
- },
8767
- required: ["taskIds"]
8768
- },
8769
- async execute(input) {
8770
- const i = input;
8771
- const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
8772
- return { summary, count: i.taskIds.length };
8773
- }
8774
- };
8775
- }
8776
- function makeTerminateTool(director) {
8777
- return {
8778
- name: "terminate_subagent",
8779
- description: 'Forcibly abort a subagent. The subagent finishes its current iteration then exits with status "stopped".',
8780
- permission: "auto",
8781
- mutating: true,
8782
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8783
- inputSchema: {
8784
- type: "object",
8785
- properties: { subagentId: { type: "string", description: "Subagent to abort." } },
8786
- required: ["subagentId"]
8787
- },
8788
- async execute(input) {
8789
- const i = input;
8790
- await director.terminate(i.subagentId);
8791
- return { ok: true };
8792
- }
8793
- };
8794
- }
8795
- function makeTerminateAllTool(director) {
8796
- return {
8797
- name: "terminate_all",
8798
- description: 'Forcibly stop every subagent in the fleet and drain the pending task queue. In-flight tasks are terminated mid-execution; pending tasks receive "aborted_by_parent" completion immediately. Use this when the fleet is wedged, looping, or you need a clean slate. Compare: work_complete stops spawning but lets running agents finish naturally.',
8799
- permission: "auto",
8800
- mutating: true,
8801
- capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
8802
- inputSchema: { type: "object", properties: {}, required: [] },
8803
- async execute() {
8804
- await director.terminateAll();
8805
- return {
8806
- ok: true,
8807
- message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.`
8808
- };
8809
- }
8810
- };
8811
- }
8812
- function makeFleetTool(director) {
8813
- return {
8814
- name: "fleet",
8815
- description: 'Fleet observation tool. Use `action` to select what you need: "status" \u2014 snapshot of all subagents + coordinator counts + pending tasks; "usage" \u2014 token + cost breakdown per subagent and totals; "health" \u2014 per-subagent budget pressure, last activity, and status; "session" \u2014 read a subagent\'s JSONL transcript (requires subagentId).',
8816
- usageHint: 'action: "status" (default) | "usage" | "health" | "session".\nFor "session", pass subagentId (required) and optional tail (trailing JSONL lines).',
8817
- permission: "auto",
8818
- mutating: false,
8819
- capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
8820
- inputSchema: {
8821
- type: "object",
8822
- properties: {
8823
- action: {
8824
- type: "string",
8825
- enum: ["status", "usage", "health", "session"],
8826
- description: "Observation to retrieve (default: status)."
8827
- },
8828
- subagentId: {
8829
- type: "string",
8830
- description: 'Subagent id (required for action: "session").'
8831
- },
8832
- tail: {
8833
- type: "number",
8834
- description: 'Number of trailing JSONL lines (action: "session" only). Omit for the full transcript.'
8835
- }
8836
- }
8837
- },
8838
- async execute(input) {
8839
- const i = input ?? {};
8840
- const action = i.action ?? "status";
8841
- switch (action) {
8842
- case "status": {
8843
- const base = director.status();
8844
- const fm = director.fleetManager;
8845
- const stats = fm?.getFleetStats();
8846
- const fleetStatus = fm?.getFleetStatus();
8847
- return {
8848
- action: "status",
8849
- subagents: base.subagents,
8850
- coordinatorStats: stats ? {
8851
- total: stats.total,
8852
- running: stats.running,
8853
- idle: stats.idle,
8854
- stopped: stats.stopped
8855
- } : void 0,
8856
- pending: fleetStatus?.pending ?? [],
8857
- usage: fm?.snapshot()
8858
- };
8859
- }
8860
- case "usage": {
8861
- return { action: "usage", ...director.snapshot() };
8862
- }
8863
- case "health": {
8864
- const status = director.status();
8865
- const snapshot = director.snapshot();
8866
- const subagents = status.subagents ?? [];
8867
- const perSubagent = snapshot.perSubagent ?? {};
8868
- return {
8869
- action: "health",
8870
- subagents: subagents.map((s) => {
8871
- const usage = perSubagent[s.id];
8872
- return {
8873
- id: s.id,
8874
- status: s.status,
8875
- lastEventAt: usage?.lastEventAt,
8876
- budgetPressure: {
8877
- iterations: usage?.iterations,
8878
- toolCalls: usage?.toolCalls,
8879
- costUsd: usage?.cost
8880
- }
8881
- };
8882
- })
8883
- };
8884
- }
8885
- case "session": {
8886
- const subagentId = i.subagentId;
8887
- if (!subagentId) {
8888
- return {
8889
- action: "session",
8890
- error: 'fleet: subagentId is required for action: "session"'
8891
- };
8892
- }
8893
- const result = await director.readSession(subagentId, i.tail);
8894
- if (!result) {
8895
- return {
8896
- action: "session",
8897
- error: `fleet: transcript unavailable for "${subagentId}". Is sessionsRoot configured?`
8898
- };
8899
- }
8900
- return { action: "session", ...result };
8901
- }
8902
- default:
8903
- return {
8904
- error: `fleet: unknown action "${action}". Valid: status, usage, health, session.`
8905
- };
8906
- }
8907
- }
8908
- };
8909
- }
8910
-
8911
9164
  // src/coordination/director-tools.ts
8912
9165
  function makeSpawnTool(director, roster) {
8913
9166
  const dispatchCatalog = () => {
@@ -8940,13 +9193,13 @@ function makeSpawnTool(director, roster) {
8940
9193
  const inputSchema = {
8941
9194
  type: "object",
8942
9195
  properties: {
8943
- role: {
9196
+ description: {
8944
9197
  type: "string",
8945
- description: "Roster role id. When set, the spawn uses the matching config from the roster and ignores other fields."
9198
+ description: "What the subagent has to do, in free form. PREFER THIS over `role`: the dispatcher scores it against the capability metadata of every agent in the roster and picks the specialist, which is more reliable than recalling a role id from a list of 77. Only reach for `role` when you are certain which one you want."
8946
9199
  },
8947
- description: {
9200
+ role: {
8948
9201
  type: "string",
8949
- description: "Free-form task description. When `role` is not set, the director uses the smart dispatcher to route this to the best-matching catalog agent. Use this when you don't know the exact role name."
9202
+ description: "Roster role id. When set, the spawn uses the matching config from the roster, ignores other fields, and SKIPS DISPATCH ENTIRELY \u2014 so a half-remembered id silently costs you the specialist. Prefer `description` unless the id is certain."
8950
9203
  },
8951
9204
  name: {
8952
9205
  type: "string",
@@ -8992,7 +9245,7 @@ function makeSpawnTool(director, roster) {
8992
9245
  return {
8993
9246
  name: "spawn_subagent",
8994
9247
  description: "Create a new subagent under this director (own LLM context, own budget). NON-BLOCKING: returns a `subagentId` immediately without triggering any model call. Pair with `assign_task` to send work and `await_tasks` to retrieve the result later \u2014 this is the async-delegation pattern that lets the leader keep working while the subagent runs in its own context.",
8995
- usageHint: "Pass `role` (matches the roster), `description` (smart dispatch to best agent), or `name` + `provider`/`model`. Returns `{ subagentId }`. Use this instead of `delegate` when you want to fan out to multiple subagents or keep the leader unblocked while work runs in parallel.",
9248
+ usageHint: "Pass `description` (what the work is \u2014 dispatched to the best-matching specialist), or `role` when you are certain of the id, or `name` + `provider`/`model`. Returns `{ subagentId }`. The roster is deep and specialised: there is very likely an agent built for this exact job, so describe the work rather than defaulting to a generalist. Use this instead of `delegate` when you want to fan out to multiple subagents or keep the leader unblocked while work runs in parallel.",
8996
9249
  permission: "auto",
8997
9250
  mutating: false,
8998
9251
  capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
@@ -9496,8 +9749,9 @@ function composeDirectorPrompt(parts = {}) {
9496
9749
  const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
9497
9750
  if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
9498
9751
  if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
9499
- sections.push(`Available roles you can spawn:
9500
- ${parts.rosterSummary.trim()}`);
9752
+ sections.push(
9753
+ "Roles you can spawn. This roster is deep and specialised \u2014 before handing work to a generalist, look for the agent built for exactly this job, and prefer describing the work (`description`) over recalling an id (`role`) so the dispatcher can find it:\n" + parts.rosterSummary.trim()
9754
+ );
9501
9755
  }
9502
9756
  if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
9503
9757
  sections.push(parts.basePrompt.trim());
@@ -9534,25 +9788,35 @@ ${parts.skills.trim()}`);
9534
9788
  }
9535
9789
  return sections.join("\n\n");
9536
9790
  }
9791
+ var ROSTER_SUMMARY_MAX_CHARS = 150;
9792
+ function nameAddsInformation(roleId, name) {
9793
+ const flatten = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
9794
+ return flatten(name) !== flatten(roleId);
9795
+ }
9537
9796
  function rosterSummaryFromConfigs(roster) {
9538
9797
  const lines = [];
9539
9798
  for (const [roleId, cfg] of Object.entries(roster)) {
9540
9799
  const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
9541
- const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
9800
+ const label = nameAddsInformation(roleId, cfg.name) ? ` (${cfg.name})` : "";
9801
+ const capability = cfg.dispatch?.summary?.trim() || // Fallback headline: a role prompt that opens with a markdown heading
9802
+ // would otherwise render as "# Generic Project Agent", which reads as
9803
+ // formatting rather than as a capability.
9804
+ (cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").replace(/^#+\s*/, "").trim() : "");
9805
+ const headline = capability.length > ROSTER_SUMMARY_MAX_CHARS ? `${capability.slice(0, ROSTER_SUMMARY_MAX_CHARS - 1).trimEnd()}\u2026` : capability;
9542
9806
  const tail = headline ? ` \u2014 ${headline}` : "";
9543
- lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
9807
+ lines.push(`- ${roleId}${label}${tag}${tail}`);
9544
9808
  }
9545
9809
  return lines.join("\n");
9546
9810
  }
9547
9811
 
9548
9812
  // src/coordination/director-session.ts
9549
9813
  import * as fsp18 from "node:fs/promises";
9550
- import * as path23 from "node:path";
9814
+ import * as path24 from "node:path";
9551
9815
 
9552
9816
  // src/storage/session-store.ts
9553
9817
  import { randomUUID as randomUUID10 } from "node:crypto";
9554
9818
  import * as fsp17 from "node:fs/promises";
9555
- import * as path22 from "node:path";
9819
+ import * as path23 from "node:path";
9556
9820
 
9557
9821
  // src/security/file-permissions.ts
9558
9822
  import { chmod } from "node:fs/promises";
@@ -9886,13 +10150,13 @@ var DefaultSecretScrubber = class {
9886
10150
  import { spawn } from "node:child_process";
9887
10151
  import * as fs2 from "node:fs";
9888
10152
  import * as net from "node:net";
9889
- import * as path10 from "node:path";
10153
+ import * as path11 from "node:path";
9890
10154
  import { fileURLToPath as fileURLToPath3 } from "node:url";
9891
10155
 
9892
10156
  // src/session-catalog/endpoint.ts
9893
10157
  import { createHash as createHash2 } from "node:crypto";
9894
10158
  import * as os2 from "node:os";
9895
- import * as path9 from "node:path";
10159
+ import * as path10 from "node:path";
9896
10160
 
9897
10161
  // src/session-catalog/protocol.ts
9898
10162
  var SESSION_CATALOG_PROTOCOL_VERSION = 1;
@@ -9905,7 +10169,7 @@ function encodeSessionCatalogMessage(message) {
9905
10169
  // src/session-catalog/endpoint.ts
9906
10170
  var SESSION_CATALOG_METADATA_FILE = ".session-catalog-server.json";
9907
10171
  function normalizedPath(value) {
9908
- const resolved = path9.resolve(value);
10172
+ const resolved = path10.resolve(value);
9909
10173
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
9910
10174
  }
9911
10175
  function sessionCatalogProjectServerKey(projectDir) {
@@ -9916,10 +10180,10 @@ function sessionCatalogProjectServerEndpoint(projectDir) {
9916
10180
  if (process.platform === "win32") {
9917
10181
  return `\\\\.\\pipe\\wrongstack-session-catalog-v${SESSION_CATALOG_PROTOCOL_VERSION}-${key}`;
9918
10182
  }
9919
- return path9.join(os2.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
10183
+ return path10.join(os2.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
9920
10184
  }
9921
10185
  function sessionCatalogProjectServerMetadataPath(projectDir) {
9922
- return path9.join(projectDir, SESSION_CATALOG_METADATA_FILE);
10186
+ return path10.join(projectDir, SESSION_CATALOG_METADATA_FILE);
9923
10187
  }
9924
10188
 
9925
10189
  // src/session-catalog/client.ts
@@ -9954,7 +10218,7 @@ function resolveSessionCatalogProjectServerUrl(moduleUrl = import.meta.url, exis
9954
10218
  return availability.kind === "available" ? availability.url : null;
9955
10219
  }
9956
10220
  function normalize2(value) {
9957
- const resolved = path10.resolve(value);
10221
+ const resolved = path11.resolve(value);
9958
10222
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
9959
10223
  }
9960
10224
  function delay(ms) {
@@ -10471,12 +10735,12 @@ var color = {
10471
10735
 
10472
10736
  // src/utils/config-backup.ts
10473
10737
  import * as fs3 from "node:fs/promises";
10474
- import * as path11 from "node:path";
10738
+ import * as path12 from "node:path";
10475
10739
  function configHistoryDir(globalRoot) {
10476
- return path11.join(globalRoot, "config-history");
10740
+ return path12.join(globalRoot, "config-history");
10477
10741
  }
10478
10742
  function configSlug(absolutePath, globalRoot) {
10479
- const rel = path11.relative(globalRoot, absolutePath);
10743
+ const rel = path12.relative(globalRoot, absolutePath);
10480
10744
  const normalized = rel.replace(/\\/g, "/").replace(/\.json$/i, "");
10481
10745
  return normalized.replace(/\//g, "-");
10482
10746
  }
@@ -10492,7 +10756,7 @@ async function backupConfigFile(filePath, paths) {
10492
10756
  const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
10493
10757
  const slug = configSlug(filePath, paths.globalRoot);
10494
10758
  const backupDir = configHistoryDir(paths.globalRoot);
10495
- const backupFile = path11.join(backupDir, `${slug}-${ts}.json`);
10759
+ const backupFile = path12.join(backupDir, `${slug}-${ts}.json`);
10496
10760
  try {
10497
10761
  await fs3.mkdir(backupDir, { recursive: true });
10498
10762
  await fs3.writeFile(backupFile, currentContent, { mode: 384, encoding: "utf8" });
@@ -10559,11 +10823,6 @@ function deepMerge(base, patch, options = {}) {
10559
10823
  return out;
10560
10824
  }
10561
10825
 
10562
- // src/types/blocks.ts
10563
- function isTextBlock(b) {
10564
- return b.type === "text";
10565
- }
10566
-
10567
10826
  // src/utils/context-evidence.ts
10568
10827
  var MAX_DIGEST_CHARS = 4e3;
10569
10828
  function createContextEvidenceState() {
@@ -10790,7 +11049,7 @@ function isEmptyMessage(msg) {
10790
11049
  }
10791
11050
 
10792
11051
  // src/core/context.ts
10793
- import * as path12 from "node:path";
11052
+ import * as path13 from "node:path";
10794
11053
 
10795
11054
  // src/utils/tool-wire-compact.ts
10796
11055
  var TOOL_DESCRIPTION_MAX_CHARS = 400;
@@ -11147,6 +11406,12 @@ function getModelFamilyRatio(calibrationKey) {
11147
11406
  }
11148
11407
 
11149
11408
  // src/core/conversation-state.ts
11409
+ function hasToolResultBlock(message) {
11410
+ return message !== void 0 && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_result");
11411
+ }
11412
+ function hasToolUseBlock(message) {
11413
+ return message?.role === "assistant" && Array.isArray(message.content) && message.content.some((block) => block.type === "tool_use");
11414
+ }
11150
11415
  var ConversationState = class {
11151
11416
  ctx;
11152
11417
  listeners = /* @__PURE__ */ new Set();
@@ -11214,15 +11479,32 @@ var ConversationState = class {
11214
11479
  */
11215
11480
  overflowCount(arr) {
11216
11481
  let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
11217
- if (Context.MAX_MESSAGE_TOKENS <= 0) return drop;
11482
+ if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
11218
11483
  let total = 0;
11219
11484
  for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
11220
- if (total <= Context.MAX_MESSAGE_TOKENS) return drop;
11485
+ if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
11221
11486
  while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
11222
11487
  total -= arr[drop]?._estTokens ?? 0;
11223
11488
  drop++;
11224
11489
  }
11225
- return drop;
11490
+ return this.protocolSafeDropCount(arr, drop);
11491
+ }
11492
+ /**
11493
+ * Front eviction must not retain a `tool_result` after evicting the
11494
+ * immediately preceding assistant `tool_use`. Long tool-heavy sessions sit
11495
+ * at the retention cap, so an unsafe boundary would create a fresh orphan on
11496
+ * nearly every append and make the request-time repair discard protocol
11497
+ * history continuously.
11498
+ *
11499
+ * Move the boundary backward to retain the complete exchange for one more
11500
+ * eviction cycle. Moving it forward would also drop non-protocol text/images
11501
+ * that may share either message. The temporary one-message cap overshoot is
11502
+ * the minimum lossless representation; once enough newer messages exist, the
11503
+ * next eviction boundary naturally moves past both halves together.
11504
+ */
11505
+ protocolSafeDropCount(arr, drop) {
11506
+ if (drop <= 0 || drop >= arr.length) return drop;
11507
+ return hasToolResultBlock(arr[drop]) && hasToolUseBlock(arr[drop - 1]) ? drop - 1 : drop;
11226
11508
  }
11227
11509
  /**
11228
11510
  * Append a content block to the trailing user message's content array.
@@ -11627,6 +11909,28 @@ var Context = class _Context {
11627
11909
  return _Context.CONVERSATION_JOURNAL_MAX_BYTES + 1;
11628
11910
  }
11629
11911
  }
11912
+ _journalDropCount = 0;
11913
+ _journalDropWarnAt = 0;
11914
+ /** Throttled notice that a conversation event never reached the journal. */
11915
+ warnConversationJournalDrop(eventType) {
11916
+ this._journalDropCount++;
11917
+ const now = Date.now();
11918
+ if (now - this._journalDropWarnAt < 5e3) return;
11919
+ this._journalDropWarnAt = now;
11920
+ const dropped = this._journalDropCount;
11921
+ this._journalDropCount = 0;
11922
+ console.warn(
11923
+ JSON.stringify({
11924
+ level: "error",
11925
+ event: "session.conversation_journal_drop",
11926
+ sessionId: this.session?.id,
11927
+ eventType,
11928
+ droppedEvents: dropped,
11929
+ message: "Session writer is not draining; replay of this session will be incomplete.",
11930
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11931
+ })
11932
+ );
11933
+ }
11630
11934
  enqueueConversationJournal(event, writer) {
11631
11935
  const bytes = this.conversationJournalBytes(event);
11632
11936
  const shouldSnapshot = event.type === "messages_replaced" || this._conversationJournalQueue.length >= _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes + bytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES;
@@ -11644,18 +11948,21 @@ var Context = class _Context {
11644
11948
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
11645
11949
  this._conversationJournalQueue.splice(index, 1);
11646
11950
  }
11647
- if (snapshotBytes <= _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
11648
- this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
11649
- this._conversationJournalBytes += snapshotBytes;
11650
- }
11951
+ this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
11952
+ this._conversationJournalBytes += snapshotBytes;
11651
11953
  } else {
11652
11954
  this._conversationJournalQueue.push({ event, bytes, writer });
11653
11955
  this._conversationJournalBytes += bytes;
11654
11956
  }
11655
11957
  while (this._conversationJournalQueue.length > _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
11656
- const dropped = this._conversationJournalQueue.shift();
11958
+ const index = this._conversationJournalQueue.findIndex(
11959
+ (queued) => queued.event.type !== "messages_replaced"
11960
+ );
11961
+ if (index === -1) break;
11962
+ const [dropped] = this._conversationJournalQueue.splice(index, 1);
11657
11963
  if (!dropped) break;
11658
11964
  this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - dropped.bytes);
11965
+ this.warnConversationJournalDrop(dropped.event.type);
11659
11966
  }
11660
11967
  this.startConversationJournalDrain();
11661
11968
  }
@@ -11893,6 +12200,12 @@ var Context = class _Context {
11893
12200
  setCurrentKanbanTask(taskId, boardId) {
11894
12201
  this.currentKanbanTaskId = taskId;
11895
12202
  this.currentKanbanBoardId = boardId;
12203
+ const existing = this.meta["kanban"] && typeof this.meta["kanban"] === "object" ? this.meta["kanban"] : {};
12204
+ this.state.setMeta("kanban", {
12205
+ ...existing,
12206
+ ...taskId ? { taskId } : { taskId: void 0 },
12207
+ ...boardId ? { boardId } : { boardId: void 0 }
12208
+ });
11896
12209
  }
11897
12210
  /**
11898
12211
  * Record a comprehensive file event for the audit trail.
@@ -11986,11 +12299,11 @@ var Context = class _Context {
11986
12299
  * Returns the resolved absolute path.
11987
12300
  */
11988
12301
  setWorkingDir(dir) {
11989
- const resolved = path12.isAbsolute(dir) ? path12.resolve(dir) : path12.resolve(this.projectRoot, dir);
12302
+ const resolved = path13.isAbsolute(dir) ? path13.resolve(dir) : path13.resolve(this.projectRoot, dir);
11990
12303
  if (!this.allowOutsideProjectRoot) {
11991
- const root = path12.resolve(this.projectRoot);
11992
- const rel = path12.relative(root, resolved);
11993
- if (rel.startsWith("..") || path12.isAbsolute(rel)) {
12304
+ const root = path13.resolve(this.projectRoot);
12305
+ const rel = path13.relative(root, resolved);
12306
+ if (rel.startsWith("..") || path13.isAbsolute(rel)) {
11994
12307
  throw new Error(`Working directory "${resolved}" is outside project root "${root}"`);
11995
12308
  }
11996
12309
  }
@@ -12419,10 +12732,10 @@ function validateAgainstSchema(value, schema) {
12419
12732
  return { ok: errors.length === 0, errors };
12420
12733
  }
12421
12734
  var MAX_SCHEMA_DEPTH = 64;
12422
- function walk(value, schema, path44, errors, depth) {
12735
+ function walk(value, schema, path45, errors, depth) {
12423
12736
  if (depth > MAX_SCHEMA_DEPTH) {
12424
12737
  errors.push({
12425
- path: path44 || "<root>",
12738
+ path: path45 || "<root>",
12426
12739
  message: `schema nesting exceeds maximum depth (${MAX_SCHEMA_DEPTH})`
12427
12740
  });
12428
12741
  return;
@@ -12430,7 +12743,7 @@ function walk(value, schema, path44, errors, depth) {
12430
12743
  if (schema.enum !== void 0) {
12431
12744
  if (!enumIncludes(schema.enum, value)) {
12432
12745
  errors.push({
12433
- path: path44 || "<root>",
12746
+ path: path45 || "<root>",
12434
12747
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
12435
12748
  });
12436
12749
  return;
@@ -12439,7 +12752,7 @@ function walk(value, schema, path44, errors, depth) {
12439
12752
  if (typeof schema.type === "string") {
12440
12753
  if (!checkType(value, schema.type)) {
12441
12754
  errors.push({
12442
- path: path44 || "<root>",
12755
+ path: path45 || "<root>",
12443
12756
  message: `expected ${schema.type}, got ${describeType(value)} (${previewValue(value)})`
12444
12757
  });
12445
12758
  return;
@@ -12451,7 +12764,7 @@ function walk(value, schema, path44, errors, depth) {
12451
12764
  if (!(req in obj)) {
12452
12765
  const expected = schema.properties?.[req]?.type;
12453
12766
  errors.push({
12454
- path: joinPath(path44, req),
12767
+ path: joinPath(path45, req),
12455
12768
  message: `required property missing${typeof expected === "string" ? ` (expected ${expected})` : ""}`
12456
12769
  });
12457
12770
  }
@@ -12459,14 +12772,14 @@ function walk(value, schema, path44, errors, depth) {
12459
12772
  if (schema.properties) {
12460
12773
  for (const [key, subSchema] of Object.entries(schema.properties)) {
12461
12774
  if (key in obj) {
12462
- walk(obj[key], subSchema, joinPath(path44, key), errors, depth + 1);
12775
+ walk(obj[key], subSchema, joinPath(path45, key), errors, depth + 1);
12463
12776
  }
12464
12777
  }
12465
12778
  }
12466
12779
  }
12467
12780
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
12468
12781
  for (let i = 0; i < value.length; i++) {
12469
- walk(value[i], schema.items, `${path44}[${i}]`, errors, depth + 1);
12782
+ walk(value[i], schema.items, `${path45}[${i}]`, errors, depth + 1);
12470
12783
  }
12471
12784
  }
12472
12785
  }
@@ -12826,18 +13139,32 @@ function compileUserRegex(pattern, flags) {
12826
13139
  var MAX_SUBJECT_LEN = 64 * 1024;
12827
13140
 
12828
13141
  // src/utils/session-scoped-path.ts
12829
- import * as path13 from "node:path";
13142
+ import * as path14 from "node:path";
12830
13143
  function sessionScopedPath(dir, sessionId, suffix) {
12831
13144
  if (!sessionId || sessionId.includes("\\") || sessionId.includes("..")) {
12832
13145
  throw invalid(sessionId);
12833
13146
  }
12834
- const resolved = path13.resolve(dir, `${sessionId}${suffix}`);
12835
- const rel = path13.relative(path13.resolve(dir), resolved);
12836
- if (rel.startsWith("..") || path13.isAbsolute(rel)) {
13147
+ const resolved = path14.resolve(dir, `${sessionId}${suffix}`);
13148
+ const rel = path14.relative(path14.resolve(dir), resolved);
13149
+ if (rel.startsWith("..") || path14.isAbsolute(rel)) {
12837
13150
  throw invalid(sessionId);
12838
13151
  }
12839
13152
  return resolved;
12840
13153
  }
13154
+ var SESSION_SIDECAR_JSONL_SUFFIXES = [
13155
+ ".replay.jsonl",
13156
+ ".audit.jsonl",
13157
+ ".annotations.jsonl"
13158
+ ];
13159
+ var RESERVED_SESSION_JSONL_NAMES = /* @__PURE__ */ new Set([
13160
+ "_index.jsonl",
13161
+ "_mailbox.jsonl"
13162
+ ]);
13163
+ function isSessionTranscriptFileName(name) {
13164
+ if (!name.endsWith(".jsonl")) return false;
13165
+ if (RESERVED_SESSION_JSONL_NAMES.has(name)) return false;
13166
+ return !SESSION_SIDECAR_JSONL_SUFFIXES.some((suffix) => name.endsWith(suffix));
13167
+ }
12841
13168
  function invalid(sessionId) {
12842
13169
  return new FsError({
12843
13170
  message: `Invalid sessionId: ${sessionId}`,
@@ -13588,6 +13915,9 @@ function subjectForToolInput(toolName, input, subjectKey) {
13588
13915
  const obj = input;
13589
13916
  if (subjectKey) {
13590
13917
  const value = obj[subjectKey];
13918
+ if (Array.isArray(value)) {
13919
+ return escapeGlobSubject(JSON.stringify(value));
13920
+ }
13591
13921
  if (typeof value === "string") {
13592
13922
  if (isPathSubjectKey(subjectKey)) {
13593
13923
  const normalized = normalizePathSubject(value);
@@ -13632,7 +13962,7 @@ function subjectForToolInput(toolName, input, subjectKey) {
13632
13962
  // src/storage/file-session-writer.ts
13633
13963
  import { closeSync, createReadStream, fsyncSync, openSync, writeSync } from "node:fs";
13634
13964
  import * as fsp7 from "node:fs/promises";
13635
- import * as path14 from "node:path";
13965
+ import * as path15 from "node:path";
13636
13966
  import { createInterface } from "node:readline";
13637
13967
 
13638
13968
  // src/storage/session-workspace-checkpoints.ts
@@ -13898,7 +14228,7 @@ var FileSessionWriter = class _FileSessionWriter {
13898
14228
  this.meta = meta;
13899
14229
  this.events = events;
13900
14230
  this.resumed = opts.resumed ?? false;
13901
- this.manifestFile = opts.dir ? path14.join(opts.dir, `${path14.basename(id)}.summary.json`) : "";
14231
+ this.manifestFile = opts.dir ? path15.join(opts.dir, `${path15.basename(id)}.summary.json`) : "";
13902
14232
  this.filePath = opts.filePath ?? "";
13903
14233
  this.secretScrubber = opts.secretScrubber;
13904
14234
  this.checkpointCas = opts.checkpointCas;
@@ -14790,7 +15120,7 @@ var FileSessionWriter = class _FileSessionWriter {
14790
15120
  import { spawn as spawn2 } from "node:child_process";
14791
15121
  import { createHash as createHash3, randomUUID as randomUUID9 } from "node:crypto";
14792
15122
  import * as fsp8 from "node:fs/promises";
14793
- import * as path15 from "node:path";
15123
+ import * as path16 from "node:path";
14794
15124
 
14795
15125
  // src/storage/storage-concurrency.ts
14796
15126
  async function mapWithConcurrency(items, concurrency, mapper) {
@@ -14821,13 +15151,13 @@ function sha256(content) {
14821
15151
  return createHash3("sha256").update(content).digest("hex");
14822
15152
  }
14823
15153
  function isInside(root, target) {
14824
- const relative8 = path15.relative(root, target);
14825
- return relative8 === "" || !relative8.startsWith("..") && !path15.isAbsolute(relative8);
15154
+ const relative8 = path16.relative(root, target);
15155
+ return relative8 === "" || !relative8.startsWith("..") && !path16.isAbsolute(relative8);
14826
15156
  }
14827
15157
  function normalizeRelative(input) {
14828
- if (!input || path15.isAbsolute(input)) return null;
15158
+ if (!input || path16.isAbsolute(input)) return null;
14829
15159
  const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
14830
- const resolved = path15.posix.normalize(normalized);
15160
+ const resolved = path16.posix.normalize(normalized);
14831
15161
  if (!resolved || resolved === "." || resolved === ".." || resolved.startsWith("../")) return null;
14832
15162
  return resolved;
14833
15163
  }
@@ -14842,8 +15172,8 @@ var SessionCheckpointCas = class {
14842
15172
  projectRoot;
14843
15173
  runGit;
14844
15174
  constructor(opts) {
14845
- this.rootDir = path15.resolve(opts.rootDir);
14846
- this.projectRoot = path15.resolve(opts.projectRoot);
15175
+ this.rootDir = path16.resolve(opts.rootDir);
15176
+ this.projectRoot = path16.resolve(opts.projectRoot);
14847
15177
  this.runGit = opts.runGit ?? defaultRunGit;
14848
15178
  }
14849
15179
  async capture(_sessionId, _promptIndex) {
@@ -14868,7 +15198,7 @@ var SessionCheckpointCas = class {
14868
15198
  relativePaths,
14869
15199
  CAPTURE_CONCURRENCY,
14870
15200
  async (relative8) => {
14871
- const absolute = path15.resolve(this.projectRoot, ...relative8.split("/"));
15201
+ const absolute = path16.resolve(this.projectRoot, ...relative8.split("/"));
14872
15202
  if (!isInside(this.projectRoot, absolute)) {
14873
15203
  unresolved.push({ path: relative8, reason: "path escapes project root" });
14874
15204
  return null;
@@ -14877,8 +15207,8 @@ var SessionCheckpointCas = class {
14877
15207
  const stat15 = await fsp8.lstat(absolute);
14878
15208
  if (stat15.isSymbolicLink()) {
14879
15209
  const linkTarget = await fsp8.readlink(absolute);
14880
- const resolvedLink = path15.resolve(path15.dirname(absolute), linkTarget);
14881
- if (path15.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
15210
+ const resolvedLink = path16.resolve(path16.dirname(absolute), linkTarget);
15211
+ if (path16.isAbsolute(linkTarget) || !isInside(this.projectRoot, resolvedLink)) {
14882
15212
  unresolved.push({
14883
15213
  path: relative8,
14884
15214
  reason: "symlink target escapes project root"
@@ -14940,7 +15270,7 @@ var SessionCheckpointCas = class {
14940
15270
  };
14941
15271
  }
14942
15272
  async materialize(checkpoint, targetRoot) {
14943
- const target = path15.resolve(targetRoot);
15273
+ const target = path16.resolve(targetRoot);
14944
15274
  if (target === this.projectRoot) {
14945
15275
  throw new Error("Refusing to materialize a workspace checkpoint over the parent project root");
14946
15276
  }
@@ -14980,10 +15310,10 @@ var SessionCheckpointCas = class {
14980
15310
  try {
14981
15311
  const output = await this.safeOutputPath(target, realTarget, entry.path);
14982
15312
  if (entry.state === "symlink") {
14983
- if (path15.isAbsolute(entry.linkTarget)) {
15313
+ if (path16.isAbsolute(entry.linkTarget)) {
14984
15314
  throw new Error("absolute symlink target refused");
14985
15315
  }
14986
- const resolvedLink = path15.resolve(path15.dirname(output), entry.linkTarget);
15316
+ const resolvedLink = path16.resolve(path16.dirname(output), entry.linkTarget);
14987
15317
  if (!isInside(target, resolvedLink)) throw new Error("symlink target escapes checkpoint root");
14988
15318
  }
14989
15319
  prepared.push({
@@ -15011,7 +15341,7 @@ var SessionCheckpointCas = class {
15011
15341
  await fsp8.unlink(output).catch((err) => {
15012
15342
  if (err.code !== "ENOENT") throw err;
15013
15343
  });
15014
- await fsp8.mkdir(path15.dirname(output), { recursive: true });
15344
+ await fsp8.mkdir(path16.dirname(output), { recursive: true });
15015
15345
  await fsp8.symlink(entry.linkTarget, output);
15016
15346
  writtenFiles.push(entry.path);
15017
15347
  continue;
@@ -15028,15 +15358,15 @@ var SessionCheckpointCas = class {
15028
15358
  }
15029
15359
  objectPath(hash2) {
15030
15360
  if (!HASH_RE.test(hash2)) throw new Error(`Invalid CAS object hash: ${hash2}`);
15031
- return path15.join(this.rootDir, "objects", hash2.slice(0, 2), hash2.slice(2));
15361
+ return path16.join(this.rootDir, "objects", hash2.slice(0, 2), hash2.slice(2));
15032
15362
  }
15033
15363
  manifestPath(hash2) {
15034
15364
  if (!HASH_RE.test(hash2)) throw new Error(`Invalid checkpoint manifest hash: ${hash2}`);
15035
- return path15.join(this.rootDir, "manifests", `${hash2}.json`);
15365
+ return path16.join(this.rootDir, "manifests", `${hash2}.json`);
15036
15366
  }
15037
15367
  async putBlob(hash2, content) {
15038
15368
  const target = this.objectPath(hash2);
15039
- await fsp8.mkdir(path15.dirname(target), { recursive: true });
15369
+ await fsp8.mkdir(path16.dirname(target), { recursive: true });
15040
15370
  try {
15041
15371
  const existing = await fsp8.readFile(target);
15042
15372
  if (sha256(existing) !== hash2) throw new Error(`Corrupt CAS object collision: ${hash2}`);
@@ -15044,9 +15374,9 @@ var SessionCheckpointCas = class {
15044
15374
  } catch (err) {
15045
15375
  if (err.code !== "ENOENT") throw err;
15046
15376
  }
15047
- const temp = path15.join(
15048
- path15.dirname(target),
15049
- `.${path15.basename(target)}.${process.pid}.${randomUUID9()}.tmp`
15377
+ const temp = path16.join(
15378
+ path16.dirname(target),
15379
+ `.${path16.basename(target)}.${process.pid}.${randomUUID9()}.tmp`
15050
15380
  );
15051
15381
  let handle;
15052
15382
  try {
@@ -15107,7 +15437,7 @@ var SessionCheckpointCas = class {
15107
15437
  async safeOutputPath(target, realTarget, relative8) {
15108
15438
  const normalized = normalizeRelative(relative8);
15109
15439
  if (!normalized) throw new Error("invalid relative path");
15110
- const output = path15.resolve(target, ...normalized.split("/"));
15440
+ const output = path16.resolve(target, ...normalized.split("/"));
15111
15441
  if (!isInside(target, output)) throw new Error("path escapes checkpoint target");
15112
15442
  let probe = output;
15113
15443
  for (; ; ) {
@@ -15117,7 +15447,7 @@ var SessionCheckpointCas = class {
15117
15447
  return output;
15118
15448
  } catch (err) {
15119
15449
  if (err.code !== "ENOENT") throw err;
15120
- const parent = path15.dirname(probe);
15450
+ const parent = path16.dirname(probe);
15121
15451
  if (parent === probe) throw err;
15122
15452
  probe = parent;
15123
15453
  }
@@ -15181,13 +15511,13 @@ function generateSessionId(startedAt, _model) {
15181
15511
  }
15182
15512
 
15183
15513
  // src/storage/session-id-resolver.ts
15184
- import * as path16 from "node:path";
15514
+ import * as path17 from "node:path";
15185
15515
  function resolveSessionId(query, candidateIds) {
15186
15516
  const normalized = query.trim();
15187
15517
  if (!normalized) return { status: "missing", query: normalized };
15188
15518
  const uniqueIds = [...new Set(candidateIds)];
15189
15519
  if (uniqueIds.includes(normalized)) return { status: "resolved", id: normalized };
15190
- const leaf = (id) => path16.posix.basename(id.replace(/\\/g, "/"));
15520
+ const leaf = (id) => path17.posix.basename(id.replace(/\\/g, "/"));
15191
15521
  const exactLeafMatches = uniqueIds.filter((id) => leaf(id) === normalized);
15192
15522
  if (exactLeafMatches.length === 1) {
15193
15523
  return { status: "resolved", id: exactLeafMatches[0] };
@@ -15241,13 +15571,13 @@ function scrubPersistedSessionSummary(summary, scrubber2) {
15241
15571
  // src/storage/session-resume-validation.ts
15242
15572
  import { createHash as createHash4 } from "node:crypto";
15243
15573
  import * as fsp9 from "node:fs/promises";
15244
- import * as path17 from "node:path";
15574
+ import * as path18 from "node:path";
15245
15575
  var MAX_REVALIDATE_BYTES = 5 * 1024 * 1024;
15246
15576
  var VALIDATION_CONCURRENCY = 8;
15247
15577
  var NOTICE_PATH_LIMIT = 20;
15248
15578
  function isInside2(root, target) {
15249
- const relative8 = path17.relative(root, target);
15250
- return relative8 === "" || !relative8.startsWith("..") && !path17.isAbsolute(relative8);
15579
+ const relative8 = path18.relative(root, target);
15580
+ return relative8 === "" || !relative8.startsWith("..") && !path18.isAbsolute(relative8);
15251
15581
  }
15252
15582
  function errno(err) {
15253
15583
  return err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
@@ -15258,7 +15588,7 @@ function latestObservations(events, projectRoot) {
15258
15588
  if (event.type !== "file_observation" || typeof event.path !== "string" || event.path.length === 0 || typeof event.hash !== "string" || !/^[a-f\d]{64}$/i.test(event.hash)) {
15259
15589
  continue;
15260
15590
  }
15261
- const normalized = path17.resolve(projectRoot, event.path);
15591
+ const normalized = path18.resolve(projectRoot, event.path);
15262
15592
  latest.set(normalized, {
15263
15593
  path: normalized,
15264
15594
  hash: event.hash.toLowerCase(),
@@ -15316,7 +15646,7 @@ async function validateOne(observation, lexicalRoot, realRoot) {
15316
15646
  }
15317
15647
  }
15318
15648
  async function validateResumeFileObservations(events, projectRoot) {
15319
- const lexicalRoot = path17.resolve(projectRoot);
15649
+ const lexicalRoot = path18.resolve(projectRoot);
15320
15650
  const realRoot = await fsp9.realpath(lexicalRoot).catch(() => lexicalRoot);
15321
15651
  const observations = latestObservations(events, lexicalRoot);
15322
15652
  const results = await mapWithConcurrency(
@@ -15330,11 +15660,20 @@ async function validateResumeFileObservations(events, projectRoot) {
15330
15660
  staleFiles: results.filter((entry) => entry !== null)
15331
15661
  };
15332
15662
  }
15663
+ var RESUME_NOTICE_HEADERS = [
15664
+ "[SESSION RESUME FILE VALIDATION]",
15665
+ "[SESSION RESUME INTERRUPTED WORK]"
15666
+ ];
15667
+ function isResumeNoticeMessage(message) {
15668
+ if (message.role !== "system" || typeof message.content !== "string") return false;
15669
+ return RESUME_NOTICE_HEADERS.some((header) => message.content === header || message.content.startsWith(`${header}
15670
+ `));
15671
+ }
15333
15672
  function formatResumeValidationNotice(validation, projectRoot) {
15334
15673
  if (validation.staleFiles.length === 0) return null;
15335
- const root = path17.resolve(projectRoot);
15674
+ const root = path18.resolve(projectRoot);
15336
15675
  const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
15337
- const relative8 = path17.relative(root, entry.path);
15676
+ const relative8 = path18.relative(root, entry.path);
15338
15677
  const display = isInside2(root, entry.path) ? relative8 || "." : entry.path;
15339
15678
  return `- ${JSON.stringify(display)} [${entry.status}]`;
15340
15679
  });
@@ -15359,22 +15698,22 @@ function formatInterruptedToolNotice(pendingToolUseCount) {
15359
15698
 
15360
15699
  // src/storage/session-store/delete-session-artifacts.ts
15361
15700
  import * as fsp10 from "node:fs/promises";
15362
- import * as path19 from "node:path";
15701
+ import * as path20 from "node:path";
15363
15702
 
15364
15703
  // src/storage/session-store/paths.ts
15365
- import * as path18 from "node:path";
15704
+ import * as path19 from "node:path";
15366
15705
  function sessionPath(storeDir, id, ext) {
15367
15706
  return sessionScopedPath(storeDir, id, ext);
15368
15707
  }
15369
15708
  function shardManifestPath(storeDir, shardKey) {
15370
- return shardKey ? path18.join(storeDir, shardKey, "_manifest.json") : path18.join(storeDir, "_manifest.json");
15709
+ return shardKey ? path19.join(storeDir, shardKey, "_manifest.json") : path19.join(storeDir, "_manifest.json");
15371
15710
  }
15372
15711
  function shardKeyForSessionId(id) {
15373
- const dirName = path18.dirname(id);
15712
+ const dirName = path19.dirname(id);
15374
15713
  return dirName === "." ? "" : dirName;
15375
15714
  }
15376
15715
  async function ensureShardDir(storeDir, id) {
15377
- const dirPath = path18.dirname(sessionScopedPath(storeDir, id, ""));
15716
+ const dirPath = path19.dirname(sessionScopedPath(storeDir, id, ""));
15378
15717
  await ensureDir(dirPath);
15379
15718
  return dirPath;
15380
15719
  }
@@ -15385,9 +15724,9 @@ async function deleteSessionArtifacts({
15385
15724
  id,
15386
15725
  jsonlPath
15387
15726
  }) {
15388
- const shardDir = path19.dirname(jsonlPath);
15389
- const base = path19.basename(id);
15390
- const sessDir = path19.join(shardDir, base);
15727
+ const shardDir = path20.dirname(jsonlPath);
15728
+ const base = path20.basename(id);
15729
+ const sessDir = path20.join(shardDir, base);
15391
15730
  const deletions = [
15392
15731
  fsp10.unlink(jsonlPath),
15393
15732
  fsp10.unlink(sessionPath(rootDir, id, ".summary.json")),
@@ -15440,13 +15779,10 @@ async function assertSessionCanBeDeleted(sessionId, isSessionInUse) {
15440
15779
  function shouldSkipSessionDirectoryEntry(name) {
15441
15780
  return name.startsWith(".") && name !== ".wrongstack" || name === "shared" || name === "subagents" || name === "attachments";
15442
15781
  }
15443
- function isSessionJsonlFileName(name) {
15444
- return name.endsWith(".jsonl") && name !== "_index.jsonl";
15445
- }
15446
15782
 
15447
15783
  // src/storage/session-store/directory-session-files.ts
15448
15784
  import * as fsp11 from "node:fs/promises";
15449
- import * as path20 from "node:path";
15785
+ import * as path21 from "node:path";
15450
15786
  function sessionIdForFile(prefix, name) {
15451
15787
  const base = name.replace(/\.jsonl$/, "");
15452
15788
  return prefix ? `${prefix}/${base}` : base;
@@ -15467,13 +15803,13 @@ async function collectSessionFiles(dir, prefix = "", depth = 0) {
15467
15803
  if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
15468
15804
  if (entry.isDirectory()) {
15469
15805
  dirEntries.push(entry);
15470
- } else if (entry.isFile() && isSessionJsonlFileName(entry.name)) {
15471
- files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path20.join(dir, entry.name) });
15806
+ } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
15807
+ files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path21.join(dir, entry.name) });
15472
15808
  }
15473
15809
  }
15474
15810
  const childFileArrays = await Promise.all(
15475
15811
  dirEntries.map(
15476
- (entry) => collectSessionFiles(path20.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
15812
+ (entry) => collectSessionFiles(path21.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
15477
15813
  )
15478
15814
  );
15479
15815
  return [...childFileArrays.flat(), ...files];
@@ -15491,13 +15827,13 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
15491
15827
  if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
15492
15828
  if (entry.isDirectory()) {
15493
15829
  dirEntries.push(entry);
15494
- } else if (entry.isFile() && isSessionJsonlFileName(entry.name)) {
15830
+ } else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
15495
15831
  fileIds.push(sessionIdForFile(prefix, entry.name));
15496
15832
  }
15497
15833
  }
15498
15834
  const childIdArrays = await Promise.all(
15499
15835
  dirEntries.map(
15500
- (entry) => collectSessionIds(path20.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
15836
+ (entry) => collectSessionIds(path21.join(dir, entry.name), childPrefixFor(entry, prefix, depth), depth + 1)
15501
15837
  )
15502
15838
  );
15503
15839
  return [...childIdArrays.flat(), ...fileIds];
@@ -15593,13 +15929,13 @@ function inheritsIntoFork(event) {
15593
15929
 
15594
15930
  // src/storage/session-store/fork-session.ts
15595
15931
  async function forkSession(host, id, opts = {}) {
15596
- const parent = await host.load(id);
15597
- let boundary = parent.events.length - 1;
15932
+ const parentEvents = await host.readRawEvents(id);
15933
+ let boundary = parentEvents.length - 1;
15598
15934
  let targetCheckpoint;
15599
15935
  if (opts.checkpointPromptIndex !== void 0) {
15600
15936
  boundary = -1;
15601
- for (let i = 0; i < parent.events.length; i++) {
15602
- const event = parent.events[i];
15937
+ for (let i = 0; i < parentEvents.length; i++) {
15938
+ const event = parentEvents[i];
15603
15939
  if (event?.type === "checkpoint" && event.promptIndex === opts.checkpointPromptIndex) {
15604
15940
  boundary = i;
15605
15941
  targetCheckpoint = event;
@@ -15609,15 +15945,18 @@ async function forkSession(host, id, opts = {}) {
15609
15945
  throw new Error(`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`);
15610
15946
  }
15611
15947
  }
15612
- const parentPrefix = parent.events.slice(0, boundary + 1);
15948
+ const parentPrefix = parentEvents.slice(0, boundary + 1);
15613
15949
  const workspaceCheckpoint = targetCheckpoint?.workspaceCheckpoint;
15614
15950
  const checkpointHash = createHash5("sha256").update(parentPrefix.map((event) => JSON.stringify(event)).join("\n") + "\n", "utf8").digest("hex");
15615
15951
  const inherited = parentPrefix.filter(inheritsIntoFork);
15952
+ const start = parentEvents.find(
15953
+ (event) => event.type === "session_start"
15954
+ );
15616
15955
  const writer = await host.create({
15617
15956
  id: "",
15618
- title: parent.metadata.title,
15619
- model: parent.metadata.model,
15620
- provider: parent.metadata.provider
15957
+ title: "",
15958
+ model: start?.model,
15959
+ provider: start?.provider
15621
15960
  });
15622
15961
  try {
15623
15962
  await writer.append({
@@ -15723,6 +16062,17 @@ var SessionLoadCache = class {
15723
16062
  this.entries.clear();
15724
16063
  this.bytes = 0;
15725
16064
  }
16065
+ /**
16066
+ * A hit hands back fresh `messages` / `events` arrays over the cached
16067
+ * contents.
16068
+ *
16069
+ * The entry outlives every caller, and callers treat what they get as their
16070
+ * own: `resume()` passes `messages` straight into a live conversation, and
16071
+ * anything walking `events` may splice it. Returning the cached arrays
16072
+ * themselves let one caller's edit rewrite what the next one loads. The
16073
+ * elements are still shared — copying them would defeat the cache — so
16074
+ * entries remain read-only *contents* behind private containers.
16075
+ */
15726
16076
  getFresh(id, stat15, full) {
15727
16077
  const cached = this.entries.get(id);
15728
16078
  if (!cached || cached.mtimeMs !== stat15.mtimeMs || cached.size !== stat15.size) {
@@ -15730,8 +16080,11 @@ var SessionLoadCache = class {
15730
16080
  }
15731
16081
  this.entries.delete(id);
15732
16082
  this.entries.set(id, cached);
15733
- if (full) return cached.data;
15734
- return { ...cached.data, messages: [] };
16083
+ return {
16084
+ ...cached.data,
16085
+ messages: full ? [...cached.data.messages] : [],
16086
+ events: [...cached.data.events]
16087
+ };
15735
16088
  }
15736
16089
  set(id, stat15, data) {
15737
16090
  this.delete(id);
@@ -15920,6 +16273,9 @@ function stripSnapshotPayload(event) {
15920
16273
  event.messagesOmitted = event.messages.length;
15921
16274
  event.messages = [];
15922
16275
  }
16276
+ function isStrippedSnapshot(event) {
16277
+ return event.messages.length === 0 && typeof event.messagesOmitted === "number" && event.messagesOmitted > 0;
16278
+ }
15923
16279
  function isSessionEventLike(value) {
15924
16280
  return value !== null && typeof value === "object" && typeof value.type === "string" && typeof value.ts === "string";
15925
16281
  }
@@ -15949,7 +16305,12 @@ function replaySessionEvent(params) {
15949
16305
  emitDamaged(params, `Ignored malformed message_updated event at index ${ev.index}`);
15950
16306
  }
15951
16307
  } else if (ev.type === "messages_replaced" && ev.version === 1) {
15952
- if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
16308
+ if (isStrippedSnapshot(ev)) {
16309
+ emitDamaged(
16310
+ params,
16311
+ `Ignored messages_replaced event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
16312
+ );
16313
+ } else if (applyContextSnapshot(messages, openToolUses, ev.messages)) {
15953
16314
  exactJournalActive = true;
15954
16315
  } else {
15955
16316
  emitDamaged(params, "Ignored malformed messages_replaced event");
@@ -15965,9 +16326,16 @@ function replaySessionEvent(params) {
15965
16326
  emitDamaged(params, `Ignored malformed messages_dropped event (count ${String(ev.count)})`);
15966
16327
  }
15967
16328
  } else if (ev.type === "context_snapshot") {
15968
- if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
16329
+ if (isStrippedSnapshot(ev)) {
16330
+ emitDamaged(
16331
+ params,
16332
+ `Ignored context_snapshot event whose payload was stripped before persistence (${String(ev.messagesOmitted)} messages)`
16333
+ );
16334
+ } else if (!applyContextSnapshot(messages, openToolUses, ev.messages)) {
15969
16335
  emitDamaged(params, "Ignored malformed context_snapshot event");
15970
16336
  }
16337
+ } else if (ev.type === "messages_replaced" || ev.type === "message_appended" || ev.type === "message_updated" || ev.type === "messages_dropped") {
16338
+ emitDamaged(params, `Ignored ${ev.type} event with unsupported version`);
15971
16339
  } else if (!exactJournalActive && ev.type === "user_input") {
15972
16340
  openToolUses.clear();
15973
16341
  messages.push({ role: "user", content: ev.content, ts: ev.ts });
@@ -16018,15 +16386,15 @@ function emitDamaged(params, detail) {
16018
16386
 
16019
16387
  // src/storage/session-store/prune-helpers.ts
16020
16388
  import * as fsp13 from "node:fs/promises";
16021
- import * as path21 from "node:path";
16389
+ import * as path22 from "node:path";
16022
16390
  function isPrunableSessionJsonl(name) {
16023
- return name.endsWith(".jsonl") && name !== "_index.jsonl" && name !== "_mailbox.jsonl" && !name.endsWith(".replay.jsonl") && !name.endsWith(".audit.jsonl");
16391
+ return isSessionTranscriptFileName(name);
16024
16392
  }
16025
16393
  async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
16026
16394
  const cutoff = Date.now() - maxAgeDays * 864e5;
16027
16395
  let deleted = 0;
16028
16396
  const pruneFile = async (dir, name, prefix) => {
16029
- const jsonlPath = path21.join(dir, name);
16397
+ const jsonlPath = path22.join(dir, name);
16030
16398
  try {
16031
16399
  const stat15 = await fsp13.stat(jsonlPath);
16032
16400
  if (stat15.mtimeMs >= cutoff) return;
@@ -16045,7 +16413,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
16045
16413
  continue;
16046
16414
  }
16047
16415
  if (!entry.isDirectory()) continue;
16048
- const dateDir = path21.join(storeDir, entry.name);
16416
+ const dateDir = path22.join(storeDir, entry.name);
16049
16417
  const files = await fsp13.readdir(dateDir, { withFileTypes: true }).catch(() => []);
16050
16418
  for (const file of files) {
16051
16419
  if (!file.isFile() || !isPrunableSessionJsonl(file.name)) continue;
@@ -16054,7 +16422,7 @@ async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
16054
16422
  }
16055
16423
  for (const entry of entries) {
16056
16424
  if (!entry.isDirectory()) continue;
16057
- const dateDir = path21.join(storeDir, entry.name);
16425
+ const dateDir = path22.join(storeDir, entry.name);
16058
16426
  try {
16059
16427
  const remaining = await fsp13.readdir(dateDir);
16060
16428
  if (remaining.length === 0) {
@@ -16407,9 +16775,9 @@ var DefaultSessionStore = class _DefaultSessionStore {
16407
16775
  static LIST_SCAN_CONCURRENCY = 32;
16408
16776
  constructor(opts) {
16409
16777
  this.dir = opts.dir;
16410
- this.projectRoot = opts.projectRoot ? path22.resolve(opts.projectRoot) : void 0;
16778
+ this.projectRoot = opts.projectRoot ? path23.resolve(opts.projectRoot) : void 0;
16411
16779
  this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
16412
- rootDir: path22.join(this.dir, "_cas"),
16780
+ rootDir: path23.join(this.dir, "_cas"),
16413
16781
  projectRoot: this.projectRoot
16414
16782
  }) : void 0;
16415
16783
  this.events = opts.events;
@@ -16420,7 +16788,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
16420
16788
  this.onAppendBatch = opts.onAppendBatch;
16421
16789
  const builtRuntime = import.meta.url.includes("/dist/");
16422
16790
  this.catalogClient = this.projectRoot && (builtRuntime || process.env["WRONGSTACK_SESSION_CATALOG_FORCE"] === "1") && resolveSessionCatalogProjectServerUrl() ? new SessionCatalogProjectClient({
16423
- projectDir: path22.dirname(this.dir),
16791
+ projectDir: path23.dirname(this.dir),
16424
16792
  projectRoot: this.projectRoot
16425
16793
  }) : void 0;
16426
16794
  }
@@ -16452,7 +16820,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
16452
16820
  // ── Storage event helpers ───────────────────────────────────────────────────
16453
16821
  /** Absolute path to the session index file. */
16454
16822
  get indexFile() {
16455
- return path22.join(this.dir, "_index.jsonl");
16823
+ return path23.join(this.dir, "_index.jsonl");
16456
16824
  }
16457
16825
  /** Join session ID to its absolute path within the store directory. */
16458
16826
  sessionPath(id, ext) {
@@ -16543,6 +16911,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
16543
16911
  async fork(id, opts = {}) {
16544
16912
  return forkSession(this, id, opts);
16545
16913
  }
16914
+ /**
16915
+ * Implements {@link SessionForkHost.readRawEvents} — the parent stream a fork
16916
+ * inherits, unmodified.
16917
+ *
16918
+ * Deliberately NOT `load()`: that loader empties superseded snapshot payloads
16919
+ * in place and front-drops events past its retention budget, both of which
16920
+ * are correct for reconstructing a conversation and wrong for copying a
16921
+ * journal prefix into a child. Streaming with an accept-everything predicate
16922
+ * keeps the scrubbing contract (`searchEvents` scrubs each line the same way
16923
+ * `load()` does) without either transformation.
16924
+ */
16925
+ async readRawEvents(id) {
16926
+ const hits = await this.searchEvents(id, () => true);
16927
+ return hits.map((hit) => hit.event);
16928
+ }
16546
16929
  /**
16547
16930
  * Capture the deterministic post-tool workspace identity through the store-owned CAS.
16548
16931
  */
@@ -16631,14 +17014,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
16631
17014
  ts: (/* @__PURE__ */ new Date()).toISOString()
16632
17015
  });
16633
17016
  }
17017
+ const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
16634
17018
  const resumedData = {
16635
17019
  ...data,
16636
17020
  ...resumeValidation ? { resumeValidation } : {},
16637
- ...noticeMessages.length > 0 ? { messages: [...data.messages, ...noticeMessages] } : {}
17021
+ messages: [...carriedMessages, ...noticeMessages]
16638
17022
  };
16639
17023
  let handle;
16640
17024
  try {
16641
- handle = await fsp17.open(file, "a", 384);
17025
+ handle = await openSessionForAppend(file);
16642
17026
  } catch (err) {
16643
17027
  emitSessionStoreError(this.events, canonicalId, file, "resume", toErrorMessage(err), false);
16644
17028
  throw new Error(
@@ -16665,7 +17049,7 @@ var DefaultSessionStore = class _DefaultSessionStore {
16665
17049
  // Shard directory (sessions/<date>/) — must match create() so the
16666
17050
  // .summary.json sidecar lands next to the JSONL instead of the
16667
17051
  // sessions root (where summaryFor() would never find it).
16668
- dir: path22.dirname(file),
17052
+ dir: path23.dirname(file),
16669
17053
  filePath: file,
16670
17054
  secretScrubber: this.secretScrubber,
16671
17055
  checkpointCas: this.checkpointCas,
@@ -16817,10 +17201,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
16817
17201
  const limit = criteria.limit ?? 100;
16818
17202
  if (this.catalogClient) {
16819
17203
  const records = await this.catalogClient.call("list_catalog", {
16820
- limit: Math.min(1e3, Math.max(limit, 100)),
16821
- ...criteria.titleContains ? { search: criteria.titleContains } : {}
17204
+ limit,
17205
+ ...criteria
16822
17206
  });
16823
- return this.scrubSummaries(records).filter((summary) => matchesSessionFilter(summary, criteria)).slice(0, limit);
17207
+ return this.scrubSummaries(records);
16824
17208
  }
16825
17209
  try {
16826
17210
  const indexed = await this.readIndex();
@@ -17045,11 +17429,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
17045
17429
  return shardKeys;
17046
17430
  }
17047
17431
  async readOrBuildShardManifest(shardKey) {
17048
- const cached = this.shardManifestCache.get(shardKey);
17049
- if (cached) return cached;
17050
17432
  const manifestPath = this.shardManifestPath(shardKey);
17433
+ const cached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
17434
+ if (cached) return cached;
17051
17435
  return withFileLock(manifestPath, async () => {
17052
- const lockedCached = this.shardManifestCache.get(shardKey);
17436
+ const lockedCached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
17053
17437
  if (lockedCached) return lockedCached;
17054
17438
  const entry = await readOrBuildShardManifestEntry({
17055
17439
  shardKey,
@@ -17060,12 +17444,40 @@ var DefaultSessionStore = class _DefaultSessionStore {
17060
17444
  summaryHeaderFor: (ref) => this.summaryHeaderFor(ref),
17061
17445
  summaryFor: (id) => this.summaryFor(id)
17062
17446
  });
17063
- this.shardManifestCache.set(shardKey, entry);
17447
+ try {
17448
+ const stat15 = await fsp17.stat(manifestPath);
17449
+ this.shardManifestCache.set(shardKey, {
17450
+ entry,
17451
+ mtimeMs: stat15.mtimeMs,
17452
+ size: stat15.size,
17453
+ ino: stat15.ino
17454
+ });
17455
+ } catch {
17456
+ this.shardManifestCache.delete(shardKey);
17457
+ }
17064
17458
  return entry;
17065
17459
  });
17066
17460
  }
17461
+ /**
17462
+ * Shard manifests are invalidated by other store processes via atomic
17463
+ * delete/rebuild. Validate the in-memory projection against the persisted
17464
+ * file so one long-lived process cannot retain another process's stale view.
17465
+ */
17466
+ async freshShardManifestCacheEntry(shardKey, manifestPath) {
17467
+ const cached = this.shardManifestCache.get(shardKey);
17468
+ if (!cached) return void 0;
17469
+ try {
17470
+ const stat15 = await fsp17.stat(manifestPath);
17471
+ if (stat15.mtimeMs === cached.mtimeMs && stat15.size === cached.size && stat15.ino === cached.ino) {
17472
+ return cached.entry;
17473
+ }
17474
+ } catch {
17475
+ }
17476
+ this.shardManifestCache.delete(shardKey);
17477
+ return void 0;
17478
+ }
17067
17479
  async collectSessionFilesInShard(shardKey) {
17068
- const dir = shardKey ? path22.join(this.dir, shardKey) : this.dir;
17480
+ const dir = shardKey ? path23.join(this.dir, shardKey) : this.dir;
17069
17481
  const entries = await this.collectSessionFiles(dir, shardKey);
17070
17482
  return shardKey ? entries.filter((entry) => entry.id.startsWith(`${shardKey}/`)) : entries.filter((entry) => !entry.id.includes("/"));
17071
17483
  }
@@ -17258,7 +17670,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
17258
17670
  const maintenance = this.catalogClient ? await this.catalogClient.call("acquire_maintenance", {
17259
17671
  sessionId: canonical,
17260
17672
  operation: "clear",
17261
- holderId: this.maintenanceHolderId
17673
+ holderId: this.maintenanceHolderId,
17674
+ // The catalog store runs inside the detached project daemon; without
17675
+ // our pid it cannot tell "this TUI clearing its own session" from
17676
+ // "another running wstack" and refuses every /clear as `is live`.
17677
+ holderPid: process.pid
17262
17678
  }) : void 0;
17263
17679
  await this.ensureShardDir(canonical);
17264
17680
  const file = this.sessionPath(canonical, ".jsonl");
@@ -17339,6 +17755,23 @@ var DefaultSessionStore = class _DefaultSessionStore {
17339
17755
  });
17340
17756
  }
17341
17757
  };
17758
+ async function openSessionForAppend(file) {
17759
+ const handle = await fsp17.open(file, "a+", 384);
17760
+ try {
17761
+ const stat15 = await handle.stat();
17762
+ if (stat15.size > 0) {
17763
+ const tail = Buffer.allocUnsafe(1);
17764
+ const { bytesRead } = await handle.read(tail, 0, 1, stat15.size - 1);
17765
+ if (bytesRead === 1 && tail[0] !== 10) {
17766
+ await handle.appendFile("\n", "utf8");
17767
+ }
17768
+ }
17769
+ return handle;
17770
+ } catch (err) {
17771
+ await handle.close().catch(() => void 0);
17772
+ throw err;
17773
+ }
17774
+ }
17342
17775
 
17343
17776
  // src/coordination/director-session.ts
17344
17777
  function makeDirectorSessionFactory(opts) {
@@ -17348,9 +17781,9 @@ function makeDirectorSessionFactory(opts) {
17348
17781
  let dir;
17349
17782
  if (opts.store) {
17350
17783
  store = opts.store;
17351
- dir = opts.sessionsRoot ? path23.join(opts.sessionsRoot, runId) : "(caller-managed)";
17784
+ dir = opts.sessionsRoot ? path24.join(opts.sessionsRoot, runId) : "(caller-managed)";
17352
17785
  } else if (opts.sessionsRoot) {
17353
- dir = path23.join(opts.sessionsRoot, runId);
17786
+ dir = path24.join(opts.sessionsRoot, runId);
17354
17787
  store = new DefaultSessionStore({ dir });
17355
17788
  } else {
17356
17789
  throw new Error("makeDirectorSessionFactory requires either `store` or `sessionsRoot`");
@@ -17374,7 +17807,7 @@ function makeDirectorSessionFactory(opts) {
17374
17807
  }
17375
17808
  async function readDirectorSubagentSession(args) {
17376
17809
  if (!args.sessionsRoot) return null;
17377
- const filePath = path23.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
17810
+ const filePath = path24.join(args.sessionsRoot, args.directorRunId, `${args.subagentId}.jsonl`);
17378
17811
  let raw;
17379
17812
  try {
17380
17813
  raw = await fsp18.readFile(filePath, "utf8");
@@ -21688,7 +22121,7 @@ function resolveContextWindowPolicy(config = {}, overrideMode) {
21688
22121
 
21689
22122
  // src/infrastructure/logger.ts
21690
22123
  import * as fsp20 from "node:fs/promises";
21691
- import * as path24 from "node:path";
22124
+ import * as path25 from "node:path";
21692
22125
  var LEVEL_RANK2 = {
21693
22126
  error: 0,
21694
22127
  warn: 1,
@@ -21759,7 +22192,7 @@ var DefaultLogger = class _DefaultLogger {
21759
22192
  this.stderr = opts.stderr !== false;
21760
22193
  this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
21761
22194
  if (this.file) {
21762
- const dir = path24.dirname(this.file);
22195
+ const dir = path25.dirname(this.file);
21763
22196
  this._tail = this._tail.then(async () => {
21764
22197
  await fsp20.mkdir(dir, { recursive: true });
21765
22198
  }).catch(() => void 0);
@@ -21915,6 +22348,9 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
21915
22348
  disabledTools: Object.freeze([]),
21916
22349
  autoExtendLimit: true,
21917
22350
  restrictToProjectRoot: true,
22351
+ // Off by default: the board is a record of the work, not a permit for it.
22352
+ // See ToolsConfig.kanbanGovernance for what turning it on costs and gates.
22353
+ kanbanGovernance: false,
21918
22354
  loopDetection: Object.freeze({
21919
22355
  mode: "steer-then-cut",
21920
22356
  steerThreshold: 3,
@@ -22519,7 +22955,7 @@ var KNOWN_TOKEN_NAMES = Object.values(KNOWN_TOKEN_GROUPS).flat();
22519
22955
  // src/execution/design-kit-loader.ts
22520
22956
  import { existsSync as existsSync4 } from "node:fs";
22521
22957
  import * as fs4 from "node:fs/promises";
22522
- import * as path25 from "node:path";
22958
+ import * as path26 from "node:path";
22523
22959
  import { fileURLToPath as fileURLToPath4 } from "node:url";
22524
22960
  var KIT_FILE = "KIT.md";
22525
22961
  var TOKENS_FILE = "tokens.json";
@@ -22625,7 +23061,7 @@ var DefaultDesignKitLoader = class {
22625
23061
  }
22626
23062
  for (const e of entries) {
22627
23063
  if (!e.isDirectory()) continue;
22628
- const kitFile = path25.join(dir, e.name, KIT_FILE);
23064
+ const kitFile = path26.join(dir, e.name, KIT_FILE);
22629
23065
  try {
22630
23066
  const raw = await fs4.readFile(kitFile, "utf8");
22631
23067
  const fm = parseKitFrontmatter(raw);
@@ -22700,7 +23136,7 @@ var DefaultDesignKitLoader = class {
22700
23136
  const m = await this.find(id);
22701
23137
  let tokens;
22702
23138
  if (m) {
22703
- const tokensPath = path25.join(path25.dirname(m.path), TOKENS_FILE);
23139
+ const tokensPath = path26.join(path26.dirname(m.path), TOKENS_FILE);
22704
23140
  try {
22705
23141
  const raw = await fs4.readFile(tokensPath, "utf8");
22706
23142
  tokens = JSON.parse(raw);
@@ -22762,12 +23198,12 @@ function mergeKitTokens(base, own) {
22762
23198
  }
22763
23199
  function resolveBundledDesignKitsDir() {
22764
23200
  try {
22765
- const here = path25.dirname(fileURLToPath4(import.meta.url));
23201
+ const here = path26.dirname(fileURLToPath4(import.meta.url));
22766
23202
  const candidates = [
22767
- path25.join(here, "design-kits"),
22768
- path25.join(here, "..", "design-kits"),
22769
- path25.join(here, "..", "..", "design-kits"),
22770
- path25.join(here, "..", "..", "..", "design-kits")
23203
+ path26.join(here, "design-kits"),
23204
+ path26.join(here, "..", "design-kits"),
23205
+ path26.join(here, "..", "..", "design-kits"),
23206
+ path26.join(here, "..", "..", "..", "design-kits")
22771
23207
  ];
22772
23208
  for (const c of candidates) {
22773
23209
  if (existsSync4(c)) return c;
@@ -22806,10 +23242,10 @@ function _resetDesignKitLoaderMemo() {
22806
23242
  // src/execution/design-project-store.ts
22807
23243
  import { existsSync as existsSync5 } from "node:fs";
22808
23244
  import * as fs5 from "node:fs/promises";
22809
- import * as path26 from "node:path";
23245
+ import * as path27 from "node:path";
22810
23246
  var DESIGN_DIR = ".design";
22811
23247
  function designProjectDir(projectRoot) {
22812
- return path26.join(projectRoot, DESIGN_DIR);
23248
+ return path27.join(projectRoot, DESIGN_DIR);
22813
23249
  }
22814
23250
  var RULE_FILES = ["rules.md", "RULES.md", "design.md"];
22815
23251
  var rulesCache = /* @__PURE__ */ new Map();
@@ -22824,7 +23260,7 @@ async function loadProjectDesignRules(projectRoot) {
22824
23260
  let rules;
22825
23261
  for (const name of RULE_FILES) {
22826
23262
  try {
22827
- const txt = await fs5.readFile(path26.join(designProjectDir(projectRoot), name), "utf8");
23263
+ const txt = await fs5.readFile(path27.join(designProjectDir(projectRoot), name), "utf8");
22828
23264
  if (txt.trim()) {
22829
23265
  rules = txt.trim();
22830
23266
  break;
@@ -22850,7 +23286,7 @@ function parseOverrides(value) {
22850
23286
  }
22851
23287
  async function loadActiveKit(projectRoot) {
22852
23288
  try {
22853
- const raw = await fs5.readFile(path26.join(designProjectDir(projectRoot), "active.json"), "utf8");
23289
+ const raw = await fs5.readFile(path27.join(designProjectDir(projectRoot), "active.json"), "utf8");
22854
23290
  const parsed = JSON.parse(raw);
22855
23291
  if (parsed && typeof parsed.kit === "string") {
22856
23292
  return {
@@ -22884,7 +23320,7 @@ function applyTokenOverrides(tokens, overrides) {
22884
23320
  async function ensureDesignDir(projectRoot) {
22885
23321
  const dir = designProjectDir(projectRoot);
22886
23322
  await fs5.mkdir(dir, { recursive: true });
22887
- const gi = path26.join(dir, ".gitignore");
23323
+ const gi = path27.join(dir, ".gitignore");
22888
23324
  if (!existsSync5(gi)) {
22889
23325
  try {
22890
23326
  await fs5.writeFile(gi, "*\n");
@@ -22898,11 +23334,11 @@ async function recordKitChoice(projectRoot, kit, stack, source, isoTime, overrid
22898
23334
  const dir = await ensureDesignDir(projectRoot);
22899
23335
  const record = { kit, stack: stack ?? null };
22900
23336
  if (overrides && Object.keys(overrides).length > 0) record.overrides = overrides;
22901
- await fs5.writeFile(path26.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
23337
+ await fs5.writeFile(path27.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
22902
23338
  `);
22903
23339
  const line = `- ${isoTime} \xB7 kit=${kit}${stack ? ` stack=${stack}` : ""} \xB7 via=${source}
22904
23340
  `;
22905
- await fs5.appendFile(path26.join(dir, "decisions.md"), line);
23341
+ await fs5.appendFile(path27.join(dir, "decisions.md"), line);
22906
23342
  } catch {
22907
23343
  }
22908
23344
  }
@@ -22918,11 +23354,11 @@ async function recordOverrides(projectRoot, patch, isoTime) {
22918
23354
  const dir = await ensureDesignDir(projectRoot);
22919
23355
  const record = { kit: active.kit, stack: active.stack ?? null };
22920
23356
  if (Object.keys(merged).length > 0) record.overrides = merged;
22921
- await fs5.writeFile(path26.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
23357
+ await fs5.writeFile(path27.join(dir, "active.json"), `${JSON.stringify(record, null, 2)}
22922
23358
  `);
22923
23359
  const keys = Object.keys(patch).join(",");
22924
23360
  await fs5.appendFile(
22925
- path26.join(dir, "decisions.md"),
23361
+ path27.join(dir, "decisions.md"),
22926
23362
  `- ${isoTime} \xB7 kit=${active.kit} \xB7 override=${keys} \xB7 via=set
22927
23363
  `
22928
23364
  );
@@ -22932,7 +23368,7 @@ async function recordOverrides(projectRoot, patch, isoTime) {
22932
23368
  }
22933
23369
  async function clearPersistedActiveKit(projectRoot) {
22934
23370
  try {
22935
- await fs5.rm(path26.join(designProjectDir(projectRoot), "active.json"), { force: true });
23371
+ await fs5.rm(path27.join(designProjectDir(projectRoot), "active.json"), { force: true });
22936
23372
  } catch {
22937
23373
  }
22938
23374
  }
@@ -23072,12 +23508,12 @@ function verifyFiles(tokens, files) {
23072
23508
  const violations = [];
23073
23509
  let onPalette = 0;
23074
23510
  let offPalette = 0;
23075
- for (const { path: path44, text } of files) {
23511
+ for (const { path: path45, text } of files) {
23076
23512
  const lines = text.split("\n");
23077
23513
  lines.forEach((lineText, i) => {
23078
23514
  const lineNo = i + 1;
23079
23515
  const flag = (snippet, reason, axis = "color") => {
23080
- violations.push({ file: path44, line: lineNo, snippet: snippet.slice(0, 80), reason, axis });
23516
+ violations.push({ file: path45, line: lineNo, snippet: snippet.slice(0, 80), reason, axis });
23081
23517
  };
23082
23518
  for (const re of [HEX_RE, FUNC_COLOR_RE]) {
23083
23519
  re.lastIndex = 0;
@@ -26960,7 +27396,7 @@ Summarize the following message range:`;
26960
27396
 
26961
27397
  // src/execution/skill-loader.ts
26962
27398
  import * as fs6 from "node:fs/promises";
26963
- import * as path27 from "node:path";
27399
+ import * as path28 from "node:path";
26964
27400
 
26965
27401
  // src/skills/foreign-sources.ts
26966
27402
  var FOREIGN_SKILL_TOOLS = [
@@ -27024,7 +27460,7 @@ async function entryIsDirectory(dir, entry) {
27024
27460
  if (entry.isDirectory()) return true;
27025
27461
  if (entry.isSymbolicLink()) {
27026
27462
  try {
27027
- return (await fs6.stat(path27.join(dir, entry.name))).isDirectory();
27463
+ return (await fs6.stat(path28.join(dir, entry.name))).isDirectory();
27028
27464
  } catch {
27029
27465
  return false;
27030
27466
  }
@@ -27048,7 +27484,7 @@ var DefaultSkillLoader = class {
27048
27484
  for (const tool of FOREIGN_SKILL_TOOLS) {
27049
27485
  if (!foreignIds.includes(tool.id)) continue;
27050
27486
  dirs.push({
27051
- dir: path27.join(root, "." + tool.id, tool.subdir),
27487
+ dir: path28.join(root, "." + tool.id, tool.subdir),
27052
27488
  source: "foreign",
27053
27489
  originTool: tool.id
27054
27490
  });
@@ -27078,7 +27514,7 @@ var DefaultSkillLoader = class {
27078
27514
  );
27079
27515
  for (const e of entries) {
27080
27516
  if (!await entryIsDirectory(dir, e)) continue;
27081
- const skillFile = path27.join(dir, e.name, "SKILL.md");
27517
+ const skillFile = path28.join(dir, e.name, "SKILL.md");
27082
27518
  let raw;
27083
27519
  try {
27084
27520
  raw = await fs6.readFile(skillFile, "utf8");
@@ -27210,7 +27646,7 @@ var DefaultSkillLoader = class {
27210
27646
  if (cached !== void 0) return cached;
27211
27647
  const m = await this.find(name);
27212
27648
  if (!m) throw new Error(`Skill "${name}" not found`);
27213
- const savePath = path27.join(path27.dirname(m.path), "SKILL.save.md");
27649
+ const savePath = path28.join(path28.dirname(m.path), "SKILL.save.md");
27214
27650
  let result;
27215
27651
  try {
27216
27652
  result = await fs6.readFile(savePath, "utf8");
@@ -27244,11 +27680,11 @@ function parseDescriptionFromText(desc) {
27244
27680
 
27245
27681
  // src/execution/prompt-loader.ts
27246
27682
  import * as fs8 from "node:fs/promises";
27247
- import * as path29 from "node:path";
27683
+ import * as path30 from "node:path";
27248
27684
 
27249
27685
  // src/storage/prompt-store.ts
27250
27686
  import * as fs7 from "node:fs/promises";
27251
- import * as path28 from "node:path";
27687
+ import * as path29 from "node:path";
27252
27688
  var SCHEMA_VERSION = 2;
27253
27689
  function migratePromptEntry(raw) {
27254
27690
  if (!raw || typeof raw !== "object") return null;
@@ -27314,7 +27750,7 @@ var DefaultPromptStore = class {
27314
27750
  if (!file.endsWith(".json")) continue;
27315
27751
  try {
27316
27752
  const raw = JSON.parse(
27317
- await fs7.readFile(path28.join(this.dir, file), "utf8")
27753
+ await fs7.readFile(path29.join(this.dir, file), "utf8")
27318
27754
  );
27319
27755
  const migrated = migratePromptEntry(raw.entry);
27320
27756
  if (migrated) entries.push(migrated);
@@ -27328,7 +27764,7 @@ var DefaultPromptStore = class {
27328
27764
  );
27329
27765
  }
27330
27766
  async get(id) {
27331
- const file = path28.join(this.dir, `${id}.json`);
27767
+ const file = path29.join(this.dir, `${id}.json`);
27332
27768
  try {
27333
27769
  const raw = JSON.parse(await fs7.readFile(file, "utf8"));
27334
27770
  return migratePromptEntry(raw.entry);
@@ -27338,12 +27774,12 @@ var DefaultPromptStore = class {
27338
27774
  }
27339
27775
  async save(entry) {
27340
27776
  await ensureDir(this.dir);
27341
- const file = path28.join(this.dir, `${entry.id}.json`);
27777
+ const file = path29.join(this.dir, `${entry.id}.json`);
27342
27778
  const raw = { version: SCHEMA_VERSION, entry };
27343
27779
  await atomicWrite(file, JSON.stringify(raw, null, 2));
27344
27780
  }
27345
27781
  async delete(id) {
27346
- const file = path28.join(this.dir, `${id}.json`);
27782
+ const file = path29.join(this.dir, `${id}.json`);
27347
27783
  try {
27348
27784
  await fs7.unlink(file);
27349
27785
  return true;
@@ -27432,7 +27868,7 @@ var DefaultPromptLoader = class {
27432
27868
  constructor(opts) {
27433
27869
  this.projectStore = typeof opts.paths.inProjectPrompts === "string" ? new DefaultPromptStore(opts.paths.inProjectPrompts) : void 0;
27434
27870
  this.userStore = typeof opts.paths.globalPrompts === "string" ? new DefaultPromptStore(opts.paths.globalPrompts) : void 0;
27435
- this.builtinDir = opts.bundledDir ? path29.join(opts.bundledDir, "prompts") : void 0;
27871
+ this.builtinDir = opts.bundledDir ? path30.join(opts.bundledDir, "prompts") : void 0;
27436
27872
  }
27437
27873
  async list() {
27438
27874
  if (this.cache) return this.cache;
@@ -27564,7 +28000,7 @@ async function walkJson(dir) {
27564
28000
  return out;
27565
28001
  }
27566
28002
  for (const e of entries) {
27567
- const full = path29.join(dir, e.name);
28003
+ const full = path30.join(dir, e.name);
27568
28004
  if (e.isDirectory()) {
27569
28005
  out.push(...await walkJson(full));
27570
28006
  } else if (e.name.endsWith(".json") && e.name !== "index.json" && e.name !== "schema.json") {
@@ -27753,7 +28189,7 @@ function readPolicy(ctx) {
27753
28189
  // src/execution/tool-executor.ts
27754
28190
  import { randomUUID as randomUUID17 } from "node:crypto";
27755
28191
  import * as fs10 from "node:fs/promises";
27756
- import * as path32 from "node:path";
28192
+ import * as path33 from "node:path";
27757
28193
  import { isDeepStrictEqual } from "node:util";
27758
28194
 
27759
28195
  // src/observability/process-telemetry.ts
@@ -27765,13 +28201,15 @@ function runWithProcessTelemetry(context, run) {
27765
28201
 
27766
28202
  // src/security/kanban-boundary.ts
27767
28203
  import { realpath as realpath3 } from "node:fs/promises";
27768
- import * as path30 from "node:path";
28204
+ import * as path31 from "node:path";
27769
28205
  import {
28206
+ evaluateContractGraphReadiness,
27770
28207
  evaluateKanbanBoundaryOpaque,
27771
28208
  evaluateKanbanBoundaryPath,
27772
28209
  readBoard,
27773
28210
  resolveKanbanBoundaryLayers
27774
28211
  } from "@wrongstack/kanban";
28212
+ var GOVERNANCE_CONTROL_TOOLS = /* @__PURE__ */ new Set(["kanban", "plan", "task", "todo"]);
27775
28213
  var PATH_KEYS = /* @__PURE__ */ new Set([
27776
28214
  "path",
27777
28215
  "paths",
@@ -27796,12 +28234,62 @@ var TOOL_PATH_KEYS = {
27796
28234
  // filesystem target in language_info. Keep ambiguous names tool-specific.
27797
28235
  language_info: ["target"]
27798
28236
  };
27799
- async function evaluateToolKanbanBoundary(tool, input, ctx) {
28237
+ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
28238
+ if (tool.name === "kanban") return { decision: "allow" };
27800
28239
  const identity = resolveKanbanIdentity(ctx);
27801
- if (!identity.boardId) return { decision: "allow" };
28240
+ const governanceRequired = options.requireGovernance && isGovernedMutation(tool, input);
28241
+ if (!identity.boardId) {
28242
+ return governanceRequired ? {
28243
+ decision: "block",
28244
+ reason: "Kanban governance is mandatory before product mutation. Create a managed card, add its required details and executable acceptance criteria, then call kanban start_task to bind it to this run."
28245
+ } : { decision: "allow" };
28246
+ }
27802
28247
  const board = await readBoard(identity.policyRoot ?? ctx.projectRoot, identity.boardId);
27803
- if (!board) return { decision: "allow" };
28248
+ if (!board) {
28249
+ return governanceRequired ? {
28250
+ decision: "block",
28251
+ reason: `Active Kanban board not found: ${identity.boardId}. Recreate or select a valid managed card before mutation.`,
28252
+ boardId: identity.boardId,
28253
+ ...identity.taskId ? { taskId: identity.taskId } : {}
28254
+ } : { decision: "allow" };
28255
+ }
27804
28256
  const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
28257
+ if (governanceRequired && board.lifecycle?.mode === "managed") {
28258
+ if (!identity.taskId) {
28259
+ return {
28260
+ decision: "block",
28261
+ reason: "Kanban governance requires an active task, not only a board. Complete the card details and call kanban start_task before mutation.",
28262
+ boardId: board.id
28263
+ };
28264
+ }
28265
+ if (!task) {
28266
+ return {
28267
+ decision: "block",
28268
+ reason: `Active Kanban task not found: ${identity.taskId}. Call kanban start_task with a valid card.`,
28269
+ boardId: board.id,
28270
+ taskId: identity.taskId
28271
+ };
28272
+ }
28273
+ const readiness = evaluateContractGraphReadiness(board, task.id);
28274
+ if (!readiness.ready) {
28275
+ return {
28276
+ decision: "block",
28277
+ reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
28278
+ boardId: board.id,
28279
+ taskId: task.id
28280
+ };
28281
+ }
28282
+ if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
28283
+ const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
28284
+ const assignmentStatus = task.assignment?.status ?? "missing";
28285
+ return {
28286
+ decision: "block",
28287
+ reason: `Active card must be in Running with a live assignment before product mutation (lifecycle: ${lifecycleStage}; assignment: ${assignmentStatus}). Call kanban start_task after completing the required card details.`,
28288
+ boardId: board.id,
28289
+ taskId: task.id
28290
+ };
28291
+ }
28292
+ }
27805
28293
  if (identity.leaseId && task?.assignment) {
27806
28294
  const caps2 = tool.capabilities ?? [];
27807
28295
  const isWrite = caps2.includes("fs.write") || caps2.includes("fs.write.outside-project") || caps2.some((c) => c.startsWith("shell."));
@@ -27837,6 +28325,19 @@ async function evaluateToolKanbanBoundary(tool, input, ctx) {
27837
28325
  ...task ? { taskId: task.id } : {}
27838
28326
  };
27839
28327
  }
28328
+ function isGovernedMutation(tool, input) {
28329
+ if (!tool.mutating || GOVERNANCE_CONTROL_TOOLS.has(tool.name)) return false;
28330
+ if (tool.capabilities?.includes("tool.meta")) return false;
28331
+ if (tool.name === "git") {
28332
+ const command = input["command"];
28333
+ if (command === "status" || command === "log" || command === "diff") return false;
28334
+ if (command === "worktree" && input["worktreeAction"] === "list") return false;
28335
+ }
28336
+ const capabilities = tool.capabilities ?? [];
28337
+ return capabilities.some(
28338
+ (capability) => capability === "fs.write" || capability === "fs.write.outside-project" || capability === "package.install" || capability === "tool.mutate.any" || capability.startsWith("shell.") || capability === "net.outbound"
28339
+ );
28340
+ }
27840
28341
  function resolveKanbanIdentity(ctx) {
27841
28342
  const metaKanban = ctx.meta["kanban"];
27842
28343
  const record = metaKanban && typeof metaKanban === "object" ? metaKanban : void 0;
@@ -27854,10 +28355,10 @@ function resolveKanbanIdentity(ctx) {
27854
28355
  async function extractCandidatePaths(toolName, input, ctx) {
27855
28356
  if (toolName === "patch" && typeof input["patch"] === "string") {
27856
28357
  const directoryInput = stringValue(input["directory"]) ?? ctx.workingDir;
27857
- const directory = path30.isAbsolute(directoryInput) ? directoryInput : path30.resolve(ctx.workingDir, directoryInput);
28358
+ const directory = path31.isAbsolute(directoryInput) ? directoryInput : path31.resolve(ctx.workingDir, directoryInput);
27858
28359
  const strip = Math.max(1, numericValue(input["strip"]) ?? 1);
27859
28360
  const targets = extractPatchTargets(input["patch"], strip).map(
27860
- (target) => relativeToProject(path30.resolve(directory, target), ctx.projectRoot)
28361
+ (target) => relativeToProject(path31.resolve(directory, target), ctx.projectRoot)
27861
28362
  );
27862
28363
  return Promise.all(targets.map((target) => canonicalizeCandidatePath(target, ctx)));
27863
28364
  }
@@ -27867,7 +28368,7 @@ async function extractCandidatePaths(toolName, input, ctx) {
27867
28368
  collectPathValues(input, values, pathKeys);
27868
28369
  if (toolName === "scaffold" && typeof input["name"] === "string") {
27869
28370
  const cwd = stringValue(input["cwd"]) ?? ctx.workingDir;
27870
- values.push(path30.join(cwd, input["name"]));
28371
+ values.push(path31.join(cwd, input["name"]));
27871
28372
  }
27872
28373
  const candidates = [
27873
28374
  ...new Set(values.flatMap(splitPathList).map((value) => resolveInputPath(value, ctx)))
@@ -27875,21 +28376,21 @@ async function extractCandidatePaths(toolName, input, ctx) {
27875
28376
  return Promise.all(candidates.map((candidate) => canonicalizeCandidatePath(candidate, ctx)));
27876
28377
  }
27877
28378
  async function canonicalizeCandidatePath(candidate, ctx) {
27878
- if (path30.isAbsolute(candidate)) return candidate;
27879
- const absolute = path30.resolve(ctx.projectRoot, candidate);
28379
+ if (path31.isAbsolute(candidate)) return candidate;
28380
+ const absolute = path31.resolve(ctx.projectRoot, candidate);
27880
28381
  const canonicalRoot = await realpath3(ctx.projectRoot).catch(() => ctx.projectRoot);
27881
28382
  let probe = absolute;
27882
28383
  const missingSegments = [];
27883
28384
  while (true) {
27884
28385
  try {
27885
- const canonical = path30.join(await realpath3(probe), ...missingSegments);
28386
+ const canonical = path31.join(await realpath3(probe), ...missingSegments);
27886
28387
  return relativeToProject(canonical, canonicalRoot);
27887
28388
  } catch (cause) {
27888
28389
  const code = cause.code;
27889
28390
  if (code !== "ENOENT" && code !== "ENOTDIR") return absolute;
27890
- const parent = path30.dirname(probe);
28391
+ const parent = path31.dirname(probe);
27891
28392
  if (parent === probe) return absolute;
27892
- missingSegments.unshift(path30.basename(probe));
28393
+ missingSegments.unshift(path31.basename(probe));
27893
28394
  probe = parent;
27894
28395
  }
27895
28396
  }
@@ -27912,12 +28413,12 @@ function splitPathList(value) {
27912
28413
  return value.split(",").map((item) => item.trim()).filter(Boolean);
27913
28414
  }
27914
28415
  function resolveInputPath(value, ctx) {
27915
- const absolute = path30.isAbsolute(value) ? value : path30.resolve(ctx.workingDir, value);
28416
+ const absolute = path31.isAbsolute(value) ? value : path31.resolve(ctx.workingDir, value);
27916
28417
  return relativeToProject(absolute, ctx.projectRoot);
27917
28418
  }
27918
28419
  function relativeToProject(absolute, projectRoot) {
27919
- const relative8 = path30.relative(projectRoot, absolute).replace(/\\/g, "/");
27920
- return relative8.startsWith("../") || path30.isAbsolute(relative8) ? absolute : relative8 || ".";
28420
+ const relative8 = path31.relative(projectRoot, absolute).replace(/\\/g, "/");
28421
+ return relative8.startsWith("../") || path31.isAbsolute(relative8) ? absolute : relative8 || ".";
27921
28422
  }
27922
28423
  function extractPatchTargets(patchText, strip) {
27923
28424
  const targets = [];
@@ -27942,7 +28443,7 @@ var GOVERNED_TOOL_EXECUTOR_META_KEY = "toolExecutor.executeGoverned";
27942
28443
  // src/execution/tool-executor-support.ts
27943
28444
  import { createHash as createHash8, randomUUID as randomUUID16 } from "node:crypto";
27944
28445
  import * as fs9 from "node:fs/promises";
27945
- import * as path31 from "node:path";
28446
+ import * as path32 from "node:path";
27946
28447
 
27947
28448
  // src/types/tool-markers.ts
27948
28449
  var MALFORMED_ARG_MARKERS = ["__raw", "__raw_arguments", "_raw"];
@@ -28067,11 +28568,11 @@ async function maybePersistLargeToolOutput(toolName, content, budget) {
28067
28568
  return content;
28068
28569
  }
28069
28570
  try {
28070
- const dir = path31.join(wstackGlobalRoot(), "tool-output");
28571
+ const dir = path32.join(wstackGlobalRoot(), "tool-output");
28071
28572
  await fs9.mkdir(dir, { recursive: true });
28072
28573
  const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
28073
28574
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
28074
- const filePath = path31.join(dir, `${stamp}-${safeTool}-${randomUUID16()}.log`);
28575
+ const filePath = path32.join(dir, `${stamp}-${safeTool}-${randomUUID16()}.log`);
28075
28576
  await fs9.writeFile(filePath, content, "utf8");
28076
28577
  const marker = `[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
28077
28578
  const fixedBytes = Buffer.byteLength(marker + TOOL_OUTPUT_ARTIFACT_OMISSION, "utf8");
@@ -28130,6 +28631,41 @@ function sliceUtf8Suffix(text, maxBytes) {
28130
28631
  return text.slice(start);
28131
28632
  }
28132
28633
 
28634
+ // src/execution/tool-error-taxonomy.ts
28635
+ function toolErrorResult(use, err, opts = {}) {
28636
+ const { category, retryable, detail } = classifyToolError(err);
28637
+ const rawMessage = err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error";
28638
+ const userMessage = opts.scrubber ? opts.scrubber(rawMessage) : rawMessage;
28639
+ const info = {
28640
+ category,
28641
+ retryable,
28642
+ userMessage,
28643
+ ...detail !== void 0 ? { detail } : {}
28644
+ };
28645
+ const prefix = formatErrorPrefix(category, retryable);
28646
+ const detailSuffix = detail ? ` [${detail}]` : "";
28647
+ return {
28648
+ type: "tool_result",
28649
+ tool_use_id: use.id,
28650
+ content: `${prefix}: ${userMessage}${detailSuffix}`,
28651
+ is_error: true,
28652
+ // Attach structured info for programmatic consumers (retry logic,
28653
+ // audit log, observability).
28654
+ _toolErrorInfo: info
28655
+ };
28656
+ }
28657
+ function formatErrorPrefix(category, retryable) {
28658
+ const label = CATEGORY_LABELS[category] ?? "Error";
28659
+ return retryable ? `${label} (retryable)` : label;
28660
+ }
28661
+ var CATEGORY_LABELS = {
28662
+ ["transient" /* TRANSIENT */]: "Transient error",
28663
+ ["not_found" /* NOT_FOUND */]: "Not found",
28664
+ ["permission" /* PERMISSION */]: "Permission denied",
28665
+ ["validation" /* VALIDATION */]: "Validation error",
28666
+ ["fatal" /* FATAL */]: "Error"
28667
+ };
28668
+
28133
28669
  // src/execution/tool-executor-logging.ts
28134
28670
  function toolLogBase(ctx, use, toolName, durationMs) {
28135
28671
  return {
@@ -28235,41 +28771,6 @@ function blockedByHookResult(use, reason) {
28235
28771
  };
28236
28772
  }
28237
28773
 
28238
- // src/execution/tool-error-taxonomy.ts
28239
- function toolErrorResult(use, err, opts = {}) {
28240
- const { category, retryable, detail } = classifyToolError(err);
28241
- const rawMessage = err instanceof Error ? err.message : typeof err === "string" ? err : "Unknown error";
28242
- const userMessage = opts.scrubber ? opts.scrubber(rawMessage) : rawMessage;
28243
- const info = {
28244
- category,
28245
- retryable,
28246
- userMessage,
28247
- ...detail !== void 0 ? { detail } : {}
28248
- };
28249
- const prefix = formatErrorPrefix(category, retryable);
28250
- const detailSuffix = detail ? ` [${detail}]` : "";
28251
- return {
28252
- type: "tool_result",
28253
- tool_use_id: use.id,
28254
- content: `${prefix}: ${userMessage}${detailSuffix}`,
28255
- is_error: true,
28256
- // Attach structured info for programmatic consumers (retry logic,
28257
- // audit log, observability).
28258
- _toolErrorInfo: info
28259
- };
28260
- }
28261
- function formatErrorPrefix(category, retryable) {
28262
- const label = CATEGORY_LABELS[category] ?? "Error";
28263
- return retryable ? `${label} (retryable)` : label;
28264
- }
28265
- var CATEGORY_LABELS = {
28266
- ["transient" /* TRANSIENT */]: "Transient error",
28267
- ["not_found" /* NOT_FOUND */]: "Not found",
28268
- ["permission" /* PERMISSION */]: "Permission denied",
28269
- ["validation" /* VALIDATION */]: "Validation error",
28270
- ["fatal" /* FATAL */]: "Error"
28271
- };
28272
-
28273
28774
  // src/execution/tool-executor-stream.ts
28274
28775
  async function executeStreamedTool({
28275
28776
  tool,
@@ -28558,7 +29059,9 @@ ${errorDetails}`,
28558
29059
  return { result, tool, durationMs: Date.now() - start };
28559
29060
  }
28560
29061
  }
28561
- const boundary = await evaluateToolKanbanBoundary(tool, use.input, ctx);
29062
+ const boundary = await evaluateToolKanbanBoundary(tool, use.input, ctx, {
29063
+ requireGovernance: this.opts.requireKanbanGovernance
29064
+ });
28562
29065
  if (boundary.decision === "block") {
28563
29066
  const result = {
28564
29067
  type: "tool_result",
@@ -28669,7 +29172,7 @@ ${errorDetails}`,
28669
29172
  const inputPath = use.input && typeof use.input === "object" ? use.input.path : void 0;
28670
29173
  const caps = tool.capabilities ?? [];
28671
29174
  const hasFileCapability = caps.includes("fs.read") || caps.includes("fs.write");
28672
- const absPath = hasFileCapability && typeof inputPath === "string" ? path32.isAbsolute(inputPath) ? inputPath : path32.resolve(ctx.projectRoot, inputPath) : void 0;
29175
+ const absPath = hasFileCapability && typeof inputPath === "string" ? path33.isAbsolute(inputPath) ? inputPath : path33.resolve(ctx.projectRoot, inputPath) : void 0;
28673
29176
  let writeTargetExisted;
28674
29177
  if (tool.name === "write" && caps.includes("fs.write") && absPath) {
28675
29178
  writeTargetExisted = await fs10.stat(absPath).then(
@@ -29545,27 +30048,27 @@ function codexModelMeta(id) {
29545
30048
 
29546
30049
  // src/models/mode-store.ts
29547
30050
  import * as fs11 from "node:fs/promises";
29548
- import * as path34 from "node:path";
30051
+ import * as path35 from "node:path";
29549
30052
 
29550
30053
  // src/types/mode-prompts.ts
29551
- import { readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
29552
- import * as path33 from "node:path";
30054
+ import { readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
30055
+ import * as path34 from "node:path";
29553
30056
  import { fileURLToPath as fileURLToPath5 } from "node:url";
29554
30057
  function modePrompt(id) {
29555
30058
  for (const dir of modePromptDirCandidates()) {
29556
30059
  try {
29557
- return readFileSync8(path33.join(dir, `${id}.md`), "utf8").trimEnd();
30060
+ return readFileSync9(path34.join(dir, `${id}.md`), "utf8").trimEnd();
29558
30061
  } catch {
29559
30062
  }
29560
30063
  }
29561
30064
  return "";
29562
30065
  }
29563
30066
  function modePromptDirCandidates() {
29564
- const here = path33.dirname(fileURLToPath5(import.meta.url));
30067
+ const here = path34.dirname(fileURLToPath5(import.meta.url));
29565
30068
  const candidates = [
29566
- path33.resolve(here, "../../instructions/modes"),
29567
- path33.resolve(here, "../instructions/modes"),
29568
- path33.resolve(here, "instructions/modes")
30069
+ path34.resolve(here, "../../instructions/modes"),
30070
+ path34.resolve(here, "../instructions/modes"),
30071
+ path34.resolve(here, "instructions/modes")
29569
30072
  ];
29570
30073
  return candidates.sort((a, b) => Number(!isDirectory3(a)) - Number(!isDirectory3(b)));
29571
30074
  }
@@ -29797,7 +30300,7 @@ var DefaultModeStore = class {
29797
30300
  }
29798
30301
  async loadActiveMode() {
29799
30302
  try {
29800
- const configPath = path34.join(this.configDir, "mode.json");
30303
+ const configPath = path35.join(this.configDir, "mode.json");
29801
30304
  const content = await fs11.readFile(configPath, "utf8");
29802
30305
  const data = JSON.parse(content);
29803
30306
  this.activeModeId = data.activeMode ?? null;
@@ -29808,7 +30311,7 @@ var DefaultModeStore = class {
29808
30311
  async saveActiveMode() {
29809
30312
  try {
29810
30313
  await fs11.mkdir(this.configDir, { recursive: true });
29811
- const configPath = path34.join(this.configDir, "mode.json");
30314
+ const configPath = path35.join(this.configDir, "mode.json");
29812
30315
  await atomicWrite(
29813
30316
  configPath,
29814
30317
  JSON.stringify({ activeMode: this.activeModeId }, null, 2)
@@ -29823,11 +30326,11 @@ async function loadProjectModes(modesDir) {
29823
30326
  const entries = await fs11.readdir(modesDir);
29824
30327
  for (const entry of entries) {
29825
30328
  if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
29826
- const filePath = path34.join(modesDir, entry);
30329
+ const filePath = path35.join(modesDir, entry);
29827
30330
  const stat15 = await fs11.stat(filePath);
29828
30331
  if (!stat15.isFile()) continue;
29829
30332
  const content = await fs11.readFile(filePath, "utf8");
29830
- const id = path34.basename(entry, path34.extname(entry));
30333
+ const id = path35.basename(entry, path35.extname(entry));
29831
30334
  modes.push({
29832
30335
  id,
29833
30336
  name: id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
@@ -29843,7 +30346,7 @@ async function loadProjectModes(modesDir) {
29843
30346
  async function loadUserModes(modesDir) {
29844
30347
  const modes = [];
29845
30348
  try {
29846
- const manifestPath = path34.join(modesDir, "modes.json");
30349
+ const manifestPath = path35.join(modesDir, "modes.json");
29847
30350
  const content = await fs11.readFile(manifestPath, "utf8");
29848
30351
  const manifest = JSON.parse(content);
29849
30352
  for (const mode of manifest.modes) {
@@ -29856,7 +30359,7 @@ async function loadUserModes(modesDir) {
29856
30359
 
29857
30360
  // src/models/models-registry.ts
29858
30361
  import * as fs12 from "node:fs/promises";
29859
- import * as path35 from "node:path";
30362
+ import * as path36 from "node:path";
29860
30363
  var DEFAULT_URL = "https://models.dev/api.json";
29861
30364
  var ENV_URL_KEY = "WRONGSTACK_MODELS_DEV_URL";
29862
30365
  var DEFAULT_TTL_SECONDS = 3 * 3600;
@@ -29933,7 +30436,7 @@ var DefaultModelsRegistry = class {
29933
30436
  this.overlay = opts.overlay;
29934
30437
  this.overlayUrl = opts.overlayUrl;
29935
30438
  this.overlayFile = opts.overlayFile;
29936
- this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path35.join(path35.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
30439
+ this.overlayCacheFile = opts.overlayCacheFile ?? (opts.overlayUrl ? path36.join(path36.dirname(opts.cacheFile), "models-overlay-cache.json") : void 0);
29937
30440
  this.logger = opts.logger ?? noOpLogger;
29938
30441
  }
29939
30442
  async load(opts = {}) {
@@ -30208,7 +30711,7 @@ var DefaultModelsRegistry = class {
30208
30711
  }
30209
30712
  /** Used by `wstack models refresh` to expose where the cache lives. */
30210
30713
  cacheLocation() {
30211
- return path35.resolve(this.cacheFile);
30714
+ return path36.resolve(this.cacheFile);
30212
30715
  }
30213
30716
  };
30214
30717
  var REASONING_EFFORTS = /* @__PURE__ */ new Set([
@@ -30759,7 +31262,7 @@ async function startMetricsServer(opts) {
30759
31262
  const tls = opts.tls;
30760
31263
  const useHttps = !!(tls?.cert && tls?.key);
30761
31264
  const host = opts.host ?? "127.0.0.1";
30762
- const path44 = opts.path ?? "/metrics";
31265
+ const path45 = opts.path ?? "/metrics";
30763
31266
  const healthPath = opts.healthPath ?? "/healthz";
30764
31267
  const healthRegistry = opts.healthRegistry;
30765
31268
  const token = opts.token;
@@ -30782,7 +31285,7 @@ async function startMetricsServer(opts) {
30782
31285
  return;
30783
31286
  }
30784
31287
  const url = req.url.split("?")[0];
30785
- if (url === path44) {
31288
+ if (url === path45) {
30786
31289
  let body;
30787
31290
  try {
30788
31291
  body = renderPrometheus(opts.sink.snapshot());
@@ -30819,9 +31322,9 @@ async function startMetricsServer(opts) {
30819
31322
  let server;
30820
31323
  if (useHttps && tls) {
30821
31324
  const { createServer } = await import("node:https");
30822
- const { readFileSync: readFileSync10 } = await import("node:fs");
31325
+ const { readFileSync: readFileSync11 } = await import("node:fs");
30823
31326
  server = createServer(
30824
- { cert: readFileSync10(tls.cert), key: readFileSync10(tls.key) },
31327
+ { cert: readFileSync11(tls.cert), key: readFileSync11(tls.key) },
30825
31328
  listener
30826
31329
  );
30827
31330
  } else {
@@ -30846,7 +31349,7 @@ async function startMetricsServer(opts) {
30846
31349
  const protocol = useHttps ? "https" : "http";
30847
31350
  return {
30848
31351
  port: boundPort,
30849
- url: `${protocol}://${host}:${boundPort}${path44}`,
31352
+ url: `${protocol}://${host}:${boundPort}${path45}`,
30850
31353
  close: () => new Promise((resolve21, reject) => {
30851
31354
  server.close((err) => err ? reject(err) : resolve21());
30852
31355
  })
@@ -31119,7 +31622,7 @@ function startOtlpTraceExporter(opts) {
31119
31622
 
31120
31623
  // src/security/permission-policy.ts
31121
31624
  import * as fs13 from "node:fs/promises";
31122
- import * as path38 from "node:path";
31625
+ import * as path39 from "node:path";
31123
31626
 
31124
31627
  // src/security/permission-policy-schema.ts
31125
31628
  var TRUST_POLICY_LIMITS = Object.freeze({
@@ -31164,19 +31667,19 @@ var UNSAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set(["__proto__", "prototype", "
31164
31667
  function isRecord5(value) {
31165
31668
  return typeof value === "object" && value !== null && !Array.isArray(value);
31166
31669
  }
31167
- function error(diagnostics, code, path44, message) {
31168
- diagnostics.push({ severity: "error", code, path: path44, message });
31670
+ function error(diagnostics, code, path45, message) {
31671
+ diagnostics.push({ severity: "error", code, path: path45, message });
31169
31672
  }
31170
- function validatePatterns(value, path44, diagnostics) {
31673
+ function validatePatterns(value, path45, diagnostics) {
31171
31674
  if (!Array.isArray(value)) {
31172
- error(diagnostics, "invalid_pattern_list", path44, "must be an array of strings");
31675
+ error(diagnostics, "invalid_pattern_list", path45, "must be an array of strings");
31173
31676
  return void 0;
31174
31677
  }
31175
31678
  if (value.length > TRUST_POLICY_LIMITS.maxPatternsPerRule) {
31176
31679
  error(
31177
31680
  diagnostics,
31178
31681
  "too_many_patterns",
31179
- path44,
31682
+ path45,
31180
31683
  `must contain at most ${TRUST_POLICY_LIMITS.maxPatternsPerRule} patterns`
31181
31684
  );
31182
31685
  return void 0;
@@ -31184,7 +31687,7 @@ function validatePatterns(value, path44, diagnostics) {
31184
31687
  const patterns = [];
31185
31688
  const seen = /* @__PURE__ */ new Set();
31186
31689
  for (const [index, pattern] of value.entries()) {
31187
- const itemPath = `${path44}[${index}]`;
31690
+ const itemPath = `${path45}[${index}]`;
31188
31691
  if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > TRUST_POLICY_LIMITS.maxPatternChars) {
31189
31692
  error(
31190
31693
  diagnostics,
@@ -31288,7 +31791,7 @@ function validateTrustPolicy(value) {
31288
31791
  }
31289
31792
 
31290
31793
  // src/security/yolo-risk.ts
31291
- import * as path36 from "node:path";
31794
+ import * as path37 from "node:path";
31292
31795
  var CATASTROPHIC_PATTERNS = [
31293
31796
  /\b(?:mkfs(?:\.[a-z0-9]+)?|mke2fs|newfs)\b/i,
31294
31797
  // make a filesystem — wipes a partition
@@ -31348,9 +31851,9 @@ function getInputString(input, key) {
31348
31851
  function pathLooksInsideProject(rawPath, projectRoot) {
31349
31852
  if (!projectRoot) return false;
31350
31853
  if (rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\")) return false;
31351
- const resolved = path36.resolve(projectRoot, rawPath);
31352
- const relative8 = path36.relative(projectRoot, resolved);
31353
- return !!relative8 && !relative8.startsWith("..") && !path36.isAbsolute(relative8);
31854
+ const resolved = path37.resolve(projectRoot, rawPath);
31855
+ const relative8 = path37.relative(projectRoot, resolved);
31856
+ return !!relative8 && !relative8.startsWith("..") && !path37.isAbsolute(relative8);
31354
31857
  }
31355
31858
  function tokenizeShell(command) {
31356
31859
  return command.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
@@ -31600,7 +32103,7 @@ var AutoApprovePermissionPolicy = class _AutoApprovePermissionPolicy {
31600
32103
 
31601
32104
  // src/security/permission-helpers.ts
31602
32105
  import { realpathSync } from "node:fs";
31603
- import * as path37 from "node:path";
32106
+ import * as path38 from "node:path";
31604
32107
  function matchesTrust(patterns, subject) {
31605
32108
  return patterns.includes(subject) || matchAny(patterns, subject);
31606
32109
  }
@@ -31684,24 +32187,24 @@ function realpathOfNearestExisting(p) {
31684
32187
  const tail = [];
31685
32188
  for (; ; ) {
31686
32189
  try {
31687
- return tail.length === 0 ? realpathSync(probe) : path37.join(realpathSync(probe), ...tail);
32190
+ return tail.length === 0 ? realpathSync(probe) : path38.join(realpathSync(probe), ...tail);
31688
32191
  } catch {
31689
- const parent = path37.dirname(probe);
32192
+ const parent = path38.dirname(probe);
31690
32193
  if (parent === probe) return p;
31691
- tail.unshift(path37.basename(probe));
32194
+ tail.unshift(path38.basename(probe));
31692
32195
  probe = parent;
31693
32196
  }
31694
32197
  }
31695
32198
  }
31696
32199
  var agentStateRootRealCache = /* @__PURE__ */ new Map();
31697
32200
  function isInsideAgentStateRoot(absPath) {
31698
- const lexicalRoot = normalizeForCompare(path37.resolve(wstackGlobalRoot()));
32201
+ const lexicalRoot = normalizeForCompare(path38.resolve(wstackGlobalRoot()));
31699
32202
  if (!lexicalRoot) return false;
31700
32203
  const target = normalizeForCompare(absPath);
31701
32204
  if (target === lexicalRoot || target.startsWith(`${lexicalRoot}/`)) return true;
31702
32205
  let realRoot = agentStateRootRealCache.get(lexicalRoot);
31703
32206
  if (realRoot === void 0) {
31704
- realRoot = normalizeForCompare(realpathOfNearestExisting(path37.resolve(wstackGlobalRoot())));
32207
+ realRoot = normalizeForCompare(realpathOfNearestExisting(path38.resolve(wstackGlobalRoot())));
31705
32208
  agentStateRootRealCache.set(lexicalRoot, realRoot);
31706
32209
  }
31707
32210
  if (!realRoot || realRoot === lexicalRoot) return false;
@@ -31710,7 +32213,7 @@ function isInsideAgentStateRoot(absPath) {
31710
32213
  }
31711
32214
  function isProtectedAgentStatePath(absPath) {
31712
32215
  if (!isInsideAgentStateRoot(absPath)) return false;
31713
- return AGENT_STATE_SENSITIVE_BASENAMES.test(path37.basename(normalizeForCompare(absPath)));
32216
+ return AGENT_STATE_SENSITIVE_BASENAMES.test(path38.basename(normalizeForCompare(absPath)));
31714
32217
  }
31715
32218
  function pathLooksSensitive(rawPath) {
31716
32219
  const normalized = stripShellQuotes(rawPath).replace(/\\/g, "/");
@@ -31871,7 +32374,7 @@ var DefaultPermissionPolicy = class {
31871
32374
  if (!hasCapability(tool, ToolCapabilities.FS_WRITE)) return false;
31872
32375
  for (const targetPath of fsWriteTargetPaths(input)) {
31873
32376
  const base = ctx.workingDir ?? ctx.cwd;
31874
- const resolved = base ? path38.resolve(base, targetPath) : path38.resolve(targetPath);
32377
+ const resolved = base ? path39.resolve(base, targetPath) : path39.resolve(targetPath);
31875
32378
  if (isInsideAgentStateRoot(resolved)) return true;
31876
32379
  }
31877
32380
  return false;
@@ -32540,7 +33043,7 @@ var DefaultPermissionPolicy = class {
32540
33043
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes3, scryptSync } from "node:crypto";
32541
33044
  import * as fs14 from "node:fs";
32542
33045
  import * as fsp22 from "node:fs/promises";
32543
- import * as path39 from "node:path";
33046
+ import * as path40 from "node:path";
32544
33047
 
32545
33048
  // src/types/secret-vault.ts
32546
33049
  var ENCRYPTED_PREFIX_PATTERN = /^enc:v(\d+):/;
@@ -32676,23 +33179,21 @@ function unwrapDataKey(buf, keyFile) {
32676
33179
  });
32677
33180
  }
32678
33181
  }
32679
- function checkKeyFilePermissions(keyFile, opts) {
32680
- if (process.platform === "win32") return;
33182
+ function keyFileNeedsHardening(keyFile, opts) {
33183
+ if (process.platform === "win32") return false;
32681
33184
  const warn = opts?.warn ?? ((msg) => console.warn(msg));
32682
33185
  try {
32683
33186
  const stat15 = fs14.statSync(keyFile);
32684
33187
  const actualMode = stat15.mode & 511;
32685
33188
  if (actualMode !== KEY_FILE_MODE) {
32686
- void restrictFilePermissions(keyFile, {
32687
- label: "secret-vault",
32688
- warn
32689
- }).catch(() => void 0);
32690
33189
  warn(
32691
33190
  `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
32692
33191
  );
33192
+ return true;
32693
33193
  }
32694
33194
  } catch {
32695
33195
  }
33196
+ return false;
32696
33197
  }
32697
33198
  function writeKeyFileAtomicSync(keyFile, content) {
32698
33199
  const tmp = `${keyFile}.${randomBytes3(4).toString("hex")}.tmp`;
@@ -32758,6 +33259,14 @@ var DefaultSecretVault = class {
32758
33259
  }).catch(() => void 0);
32759
33260
  this.pendingHardening.push(p);
32760
33261
  }
33262
+ /** Detect and schedule repair of a pre-existing POSIX key with loose mode bits. */
33263
+ checkKeyFilePermissions() {
33264
+ if (keyFileNeedsHardening(this.keyFile, {
33265
+ warn: (msg) => this.logWarn(msg)
33266
+ })) {
33267
+ this.scheduleKeyHardening();
33268
+ }
33269
+ }
32761
33270
  /** Flush all pending key-file hardening promises. */
32762
33271
  flushHardening() {
32763
33272
  if (this.pendingHardening.length === 0) return Promise.resolve();
@@ -32833,7 +33342,7 @@ var DefaultSecretVault = class {
32833
33342
  const oldVersion = this._keyVersion;
32834
33343
  const newKey = randomBytes3(KEY_BYTES);
32835
33344
  const newVersion = oldVersion + 1;
32836
- fs14.mkdirSync(path39.dirname(this.keyFile), { recursive: true });
33345
+ fs14.mkdirSync(path40.dirname(this.keyFile), { recursive: true });
32837
33346
  const passphrase = getVaultPassphrase();
32838
33347
  if (passphrase) {
32839
33348
  writeKeyFileAtomicSync(this.keyFile, wrapDataKey(newKey, newVersion, passphrase));
@@ -32845,7 +33354,7 @@ var DefaultSecretVault = class {
32845
33354
  writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
32846
33355
  }
32847
33356
  this.scheduleKeyHardening();
32848
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33357
+ this.checkKeyFilePermissions();
32849
33358
  this.key = newKey;
32850
33359
  this._keyVersion = newVersion;
32851
33360
  return { oldVersion, newVersion };
@@ -32861,12 +33370,9 @@ var DefaultSecretVault = class {
32861
33370
  const passphrase = getVaultPassphrase();
32862
33371
  if (!passphrase || !this.key) return;
32863
33372
  try {
32864
- writeKeyFileAtomicSync(
32865
- this.keyFile,
32866
- wrapDataKey(this.key, this._keyVersion, passphrase)
32867
- );
33373
+ writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
32868
33374
  this.scheduleKeyHardening();
32869
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33375
+ this.checkKeyFilePermissions();
32870
33376
  } catch {
32871
33377
  }
32872
33378
  }
@@ -32878,13 +33384,13 @@ var DefaultSecretVault = class {
32878
33384
  const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
32879
33385
  this.key = key2;
32880
33386
  this._keyVersion = version;
32881
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33387
+ this.checkKeyFilePermissions();
32882
33388
  return this.key;
32883
33389
  }
32884
33390
  if (buf.length === KEY_BYTES) {
32885
33391
  this.key = buf;
32886
33392
  this._keyVersion = 1;
32887
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33393
+ this.checkKeyFilePermissions();
32888
33394
  this.migrateToWrappedIfPassphrase();
32889
33395
  return this.key;
32890
33396
  }
@@ -32908,7 +33414,7 @@ var DefaultSecretVault = class {
32908
33414
  }
32909
33415
  this.key = Buffer.from(key2);
32910
33416
  this._keyVersion = version;
32911
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33417
+ this.checkKeyFilePermissions();
32912
33418
  this.migrateToWrappedIfPassphrase();
32913
33419
  return this.key;
32914
33420
  }
@@ -32920,7 +33426,7 @@ var DefaultSecretVault = class {
32920
33426
  } catch (err) {
32921
33427
  if (err.code !== "ENOENT") throw err;
32922
33428
  }
32923
- fs14.mkdirSync(path39.dirname(this.keyFile), { recursive: true });
33429
+ fs14.mkdirSync(path40.dirname(this.keyFile), { recursive: true });
32924
33430
  const key = randomBytes3(KEY_BYTES);
32925
33431
  const passphrase = getVaultPassphrase();
32926
33432
  const initialBytes = passphrase ? wrapDataKey(key, 1, passphrase) : key;
@@ -32935,13 +33441,13 @@ var DefaultSecretVault = class {
32935
33441
  const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
32936
33442
  this.key = winnerKey;
32937
33443
  this._keyVersion = version;
32938
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33444
+ this.checkKeyFilePermissions();
32939
33445
  return this.key;
32940
33446
  }
32941
33447
  if (buf.length === KEY_BYTES) {
32942
33448
  this.key = buf;
32943
33449
  this._keyVersion = 1;
32944
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33450
+ this.checkKeyFilePermissions();
32945
33451
  return this.key;
32946
33452
  }
32947
33453
  if (buf.length === VERSIONED_KEY_FILE_SIZE) {
@@ -32957,7 +33463,7 @@ var DefaultSecretVault = class {
32957
33463
  const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
32958
33464
  this.key = Buffer.from(winnerKey);
32959
33465
  this._keyVersion = version;
32960
- checkKeyFilePermissions(this.keyFile, { warn: (msg) => this.logWarn(msg) });
33466
+ this.checkKeyFilePermissions();
32961
33467
  return this.key;
32962
33468
  }
32963
33469
  throw new ConfigError({
@@ -32980,7 +33486,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
32980
33486
  }
32981
33487
  const merged = deepMerge(current, patch ?? {});
32982
33488
  const encrypted = encryptConfigSecrets(merged, vault);
32983
- await fsp22.mkdir(path39.dirname(configPath), { recursive: true });
33489
+ await fsp22.mkdir(path40.dirname(configPath), { recursive: true });
32984
33490
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
32985
33491
  await restrictFilePermissions2(configPath);
32986
33492
  await vault.flushHardening?.();
@@ -33035,7 +33541,7 @@ function walkCount(node, vault, counter) {
33035
33541
  // src/storage/attachment-store.ts
33036
33542
  import { randomBytes as randomBytes4 } from "node:crypto";
33037
33543
  import * as fsp23 from "node:fs/promises";
33038
- import * as path40 from "node:path";
33544
+ import * as path41 from "node:path";
33039
33545
  var DEFAULT_SPOOL_THRESHOLD = 256 * 1024;
33040
33546
  var PLACEHOLDER_RE2 = /\[(pasted|image|file) #(\d+)[^\]]*\]|\[file:([^\]]+)\]/g;
33041
33547
  var DefaultAttachmentStore = class {
@@ -33056,7 +33562,7 @@ var DefaultAttachmentStore = class {
33056
33562
  let data = input.data;
33057
33563
  if (this.spoolDir && bytes >= this.spoolThreshold) {
33058
33564
  await fsp23.mkdir(this.spoolDir, { recursive: true });
33059
- spooledPath = path40.join(this.spoolDir, `${id}.bin`);
33565
+ spooledPath = path41.join(this.spoolDir, `${id}.bin`);
33060
33566
  await atomicWrite(spooledPath, input.data, {
33061
33567
  encoding: input.kind === "image" ? "base64" : "utf8"
33062
33568
  });
@@ -33208,6 +33714,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
33208
33714
  disabledTools: DEFAULT_TOOLS_CONFIG.disabledTools,
33209
33715
  autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
33210
33716
  restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot,
33717
+ kanbanGovernance: DEFAULT_TOOLS_CONFIG.kanbanGovernance,
33211
33718
  loopDetection: DEFAULT_TOOLS_CONFIG.loopDetection
33212
33719
  },
33213
33720
  log: { level: "info" },
@@ -33592,10 +34099,18 @@ var IN_PROJECT_DENIED_PATHS = [
33592
34099
  {
33593
34100
  path: "tools.restrictToProjectRoot",
33594
34101
  reason: "The other half of the filesystem confinement switch."
34102
+ },
34103
+ {
34104
+ // Denied for the direction that loosens: the flag defaults to false, so a
34105
+ // repo can only ever use it to turn OFF a gate the user deliberately
34106
+ // enabled. Same class as `tools.restrictToProjectRoot` — a control the
34107
+ // operator owns, not the checked-out repository.
34108
+ path: "tools.kanbanGovernance",
34109
+ reason: "Repo-committed config could disable a Kanban governance gate the operator switched on, letting product mutations run outside any managed card."
33595
34110
  }
33596
34111
  ];
33597
- function deleteNestedPath(target, path44) {
33598
- const segments = path44.split(".");
34112
+ function deleteNestedPath(target, path45) {
34113
+ const segments = path45.split(".");
33599
34114
  const last = segments[segments.length - 1];
33600
34115
  if (last === void 0) return false;
33601
34116
  let cursor = target;
@@ -33652,16 +34167,16 @@ function assertInProjectAllowListComplete() {
33652
34167
  );
33653
34168
  }
33654
34169
  const orphanedPaths = IN_PROJECT_DENIED_PATHS.filter(
33655
- ({ path: path44 }) => !IN_PROJECT_ALLOWED_KEYS.has(path44.split(".")[0] ?? "")
33656
- ).map(({ path: path44 }) => path44);
34170
+ ({ path: path45 }) => !IN_PROJECT_ALLOWED_KEYS.has(path45.split(".")[0] ?? "")
34171
+ ).map(({ path: path45 }) => path45);
33657
34172
  if (orphanedPaths.length > 0) {
33658
34173
  problems.push(
33659
34174
  `IN_PROJECT_DENIED_PATHS entr(ies) whose top-level parent is not allowed: ` + orphanedPaths.join(", ") + ". The parent is already stripped wholesale, so the nested denial is dead \u2014 remove it."
33660
34175
  );
33661
34176
  }
33662
34177
  const malformedPaths = IN_PROJECT_DENIED_PATHS.filter(
33663
- ({ path: path44 }) => path44.split(".").length < 2 || path44.split(".").some((s) => s.length === 0)
33664
- ).map(({ path: path44 }) => path44);
34178
+ ({ path: path45 }) => path45.split(".").length < 2 || path45.split(".").some((s) => s.length === 0)
34179
+ ).map(({ path: path45 }) => path45);
33665
34180
  if (malformedPaths.length > 0) {
33666
34181
  problems.push(
33667
34182
  `IN_PROJECT_DENIED_PATHS entr(ies) are not dotted nested paths: ` + malformedPaths.join(", ") + ". Top-level keys belong in KNOWN_DENIED_IN_PROJECT instead."
@@ -33689,8 +34204,8 @@ function stripUnsafeInProjectFields(inProject, sourcePath, warn = (msg) => conso
33689
34204
  }
33690
34205
  stripped.push(k);
33691
34206
  }
33692
- for (const { path: path44 } of IN_PROJECT_DENIED_PATHS) {
33693
- if (deleteNestedPath(out, path44)) stripped.push(path44);
34207
+ for (const { path: path45 } of IN_PROJECT_DENIED_PATHS) {
34208
+ if (deleteNestedPath(out, path45)) stripped.push(path45);
33694
34209
  }
33695
34210
  if (stripped.length > 0) {
33696
34211
  warn(
@@ -33879,10 +34394,10 @@ function removeLegacySageEngine(config) {
33879
34394
  }
33880
34395
 
33881
34396
  // src/storage/config-loader/path-identity.ts
33882
- import * as path41 from "node:path";
34397
+ import * as path42 from "node:path";
33883
34398
  function samePath(a, b) {
33884
- let ra = path41.resolve(a);
33885
- let rb = path41.resolve(b);
34399
+ let ra = path42.resolve(a);
34400
+ let rb = path42.resolve(b);
33886
34401
  if (process.platform === "win32" || process.platform === "darwin") {
33887
34402
  ra = ra.toLowerCase();
33888
34403
  rb = rb.toLowerCase();
@@ -34564,8 +35079,8 @@ function deepFreeze(obj) {
34564
35079
  }
34565
35080
 
34566
35081
  // src/storage/plan-store.ts
34567
- import * as fsp24 from "node:fs/promises";
34568
35082
  import { randomUUID as randomUUID18 } from "node:crypto";
35083
+ import * as fsp24 from "node:fs/promises";
34569
35084
  async function loadPlan(filePath, events) {
34570
35085
  const t0 = Date.now();
34571
35086
  let raw;
@@ -34863,7 +35378,7 @@ ${cat}:`);
34863
35378
 
34864
35379
  // src/storage/queue-store.ts
34865
35380
  import * as fsp25 from "node:fs/promises";
34866
- import * as path42 from "node:path";
35381
+ import * as path43 from "node:path";
34867
35382
  var QUEUE_MAX_ITEMS = 100;
34868
35383
  var QUEUE_MAX_BYTES = 16 * 1024 * 1024;
34869
35384
  var QUEUE_MAX_ITEM_BYTES = 8 * 1024 * 1024;
@@ -34896,7 +35411,7 @@ var QueueStore = class {
34896
35411
  traceId;
34897
35412
  logger;
34898
35413
  constructor(opts) {
34899
- this.file = path42.join(opts.dir, "queue.json");
35414
+ this.file = path43.join(opts.dir, "queue.json");
34900
35415
  this.events = opts.events;
34901
35416
  this.traceId = opts.traceId;
34902
35417
  this.logger = opts.logger;
@@ -35112,7 +35627,7 @@ function isPersistedQueueItem(v) {
35112
35627
  // src/storage/recovery-lock.ts
35113
35628
  import * as fsp26 from "node:fs/promises";
35114
35629
  import * as os3 from "node:os";
35115
- import * as path43 from "node:path";
35630
+ import * as path44 from "node:path";
35116
35631
  var LOCK_FILE = "active.json";
35117
35632
  var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
35118
35633
  var RecoveryLock = class {
@@ -35123,7 +35638,7 @@ var RecoveryLock = class {
35123
35638
  sessionStore;
35124
35639
  probe;
35125
35640
  constructor(opts) {
35126
- this.file = path43.join(opts.dir, LOCK_FILE);
35641
+ this.file = path44.join(opts.dir, LOCK_FILE);
35127
35642
  this.pid = opts.pid ?? process.pid;
35128
35643
  this.hostname = opts.hostname ?? os3.hostname();
35129
35644
  this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
@@ -35197,7 +35712,7 @@ var RecoveryLock = class {
35197
35712
  * null return before calling this.
35198
35713
  */
35199
35714
  async write(sessionId) {
35200
- await ensureDir(path43.dirname(this.file));
35715
+ await ensureDir(path44.dirname(this.file));
35201
35716
  const lock = {
35202
35717
  v: 1,
35203
35718
  sessionId,