@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.
- package/README.md +1 -1
- package/dist/chronicle/index.js +117 -30
- package/dist/chronicle/project-server.js +12 -5
- package/dist/coordination/agents/index.js +4645 -3448
- package/dist/coordination/agents/project-agent-auto-optimize.d.ts +116 -0
- package/dist/coordination/agents/project-agent-capture-window.d.ts +29 -0
- package/dist/coordination/agents/project-agent-config-io.d.ts +11 -0
- package/dist/coordination/agents/project-agent-consolidation.d.ts +34 -2
- package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
- package/dist/coordination/agents/project-agent-files.d.ts +12 -3
- package/dist/coordination/agents/project-agent-identity-types.d.ts +4 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +38 -11
- package/dist/coordination/agents/project-agent-learning-entries.d.ts +8 -2
- package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
- package/dist/coordination/agents/project-agent-learning-structured.d.ts +72 -1
- package/dist/coordination/agents/project-agent-optimizer.d.ts +49 -0
- package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
- package/dist/coordination/agents/project-agent-skill-layer.d.ts +146 -0
- package/dist/coordination/agents/role-skills.d.ts +11 -1
- package/dist/coordination/agents/types.d.ts +10 -2
- package/dist/coordination/director-prompts.d.ts +19 -6
- package/dist/coordination/director-tools.d.ts +2 -2
- package/dist/coordination/fleet.d.ts +0 -6
- package/dist/coordination/index.d.ts +1 -1
- package/dist/coordination/index.js +5746 -4326
- package/dist/coordination/mail-tools.d.ts +1 -1
- package/dist/core/agent-types.d.ts +4 -2
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/context.d.ts +19 -0
- package/dist/core/conversation-state.d.ts +14 -0
- package/dist/core/fallback-profile-manager.d.ts +70 -2
- package/dist/core/index.js +317 -108
- package/dist/core/system-prompt-blocks.d.ts +1 -1
- package/dist/core/system-prompt-builder.d.ts +13 -1
- package/dist/core/system-prompt-glossary.d.ts +73 -0
- package/dist/core/system-prompt-memory-skills.d.ts +2 -2
- package/dist/defaults/index.js +1635 -1120
- package/dist/execution/council-orchestrator.d.ts +3 -13
- package/dist/execution/index.js +3136 -2706
- package/dist/execution/one-shot-llm.d.ts +5 -0
- package/dist/goal/index.js +7 -0
- package/dist/hq/index.js +17 -7
- package/dist/hq/protocol/kanban.d.ts +21 -0
- package/dist/hq/protocol.js +5 -1
- package/dist/hq/redaction.d.ts +14 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5502 -3483
- package/dist/infrastructure/index.js +247 -122
- package/dist/kernel/events/agent-events.d.ts +28 -0
- package/dist/plugin/index.js +111 -7
- package/dist/registry/index.js +11 -0
- package/dist/registry/tool-registry.d.ts +8 -0
- package/dist/replay/hash.d.ts +9 -0
- package/dist/replay/index.js +14 -4
- package/dist/replay/replay-provider-runner.d.ts +31 -1
- package/dist/security/index.js +92 -21
- package/dist/security/kanban-boundary.d.ts +5 -1
- package/dist/security/secret-vault.d.ts +2 -0
- package/dist/session-catalog/index.js +86 -10
- package/dist/session-catalog/project-server.js +100 -17
- package/dist/session-catalog/protocol.d.ts +20 -4
- package/dist/session-catalog/store.d.ts +19 -3
- package/dist/storage/index.js +305 -69
- package/dist/storage/memory-consolidator.d.ts +4 -2
- package/dist/storage/plan-store.d.ts +1 -1
- package/dist/storage/session-resume-validation.d.ts +24 -0
- package/dist/storage/session-store/directory-scan.d.ts +5 -1
- package/dist/storage/session-store/fork-session.d.ts +13 -1
- package/dist/storage/session-store/load-cache.d.ts +11 -0
- package/dist/storage/session-store/prune-helpers.d.ts +5 -0
- package/dist/storage/session-store.d.ts +18 -0
- package/dist/tasking/index.js +5 -0
- package/dist/tools/index.js +1151 -831
- package/dist/types/config/mcp-features.d.ts +31 -1
- package/dist/types/config/root.d.ts +23 -1
- package/dist/types/config/skills-fleet-brain.d.ts +34 -0
- package/dist/types/config/tools.d.ts +22 -0
- package/dist/types/config/ui.d.ts +14 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/default-config.d.ts +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +23 -0
- package/dist/types/multi-agent.d.ts +7 -0
- package/dist/types/session.d.ts +9 -1
- package/dist/types/task-graph.d.ts +2 -0
- package/dist/types/tool-executor.d.ts +2 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +217 -76
- package/dist/utils/project-state-guard.d.ts +21 -0
- package/dist/utils/session-scoped-path.d.ts +17 -0
- package/dist/utils/todos-format.d.ts +20 -0
- package/instructions/leader-after-task.md +3 -4
- package/instructions/system-lite.md +17 -14
- package/instructions/system-pro.md +29 -30
- package/instructions/system.md +29 -28
- package/package.json +3 -3
- package/skills/wrongstack-kanban/SKILL.md +95 -93
package/dist/defaults/index.js
CHANGED
|
@@ -1277,8 +1277,8 @@ function defaultFormatTaskInput(task) {
|
|
|
1277
1277
|
}
|
|
1278
1278
|
|
|
1279
1279
|
// src/coordination/agents/agent-prompts.ts
|
|
1280
|
-
import { readFileSync as
|
|
1281
|
-
import * as
|
|
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
|
|
1425
|
-
import * as
|
|
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 =
|
|
2306
|
+
const identityPath = path5.join(roleDir(role, projectRoot), "identity.md");
|
|
1837
2307
|
try {
|
|
1838
|
-
return
|
|
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 =
|
|
2314
|
+
const learnedPath = path5.join(roleDir(role, projectRoot), "learned.md");
|
|
1845
2315
|
try {
|
|
1846
|
-
return
|
|
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 =
|
|
2322
|
+
const projectPath = path5.join(roleDir(role, projectRoot), "knowledge.json");
|
|
1853
2323
|
try {
|
|
1854
|
-
const raw =
|
|
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
|
|
1885
|
-
|
|
2355
|
+
const rawEntries = parseStructuredLearnedEntriesFromContent(
|
|
2356
|
+
rawLearned,
|
|
2357
|
+
splitLearnedEntries(rawLearned)
|
|
2358
|
+
);
|
|
1886
2359
|
const rawBytes = Buffer.byteLength(rawLearned, "utf8");
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
${
|
|
1898
|
-
|
|
1899
|
-
learnedContent = rawLearned;
|
|
1900
|
-
}
|
|
2373
|
+
${delta}`;
|
|
2374
|
+
learnedLabel = "Consolidated knowledge for this project";
|
|
1901
2375
|
} else {
|
|
1902
|
-
learnedContent =
|
|
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 =
|
|
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 =
|
|
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
|
|
1967
|
-
const
|
|
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
|
-
##
|
|
2475
|
+
## Project-developed skills
|
|
1972
2476
|
|
|
1973
|
-
|
|
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 =
|
|
2566
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
2064
2567
|
const profileInstructions = resolveWstackPaths({ projectRoot: process.cwd() }).globalInstructions;
|
|
2065
2568
|
return [
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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 =
|
|
2579
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
2077
2580
|
const roots = [
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
2642
|
+
const here = path6.dirname(fileURLToPath(import.meta.url));
|
|
2140
2643
|
const explicitDir = envDir || void 0;
|
|
2141
2644
|
const candidates = [
|
|
2142
|
-
...explicitDir ? [
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
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
|
-
/**
|
|
2206
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
7922
|
-
import * as
|
|
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 =
|
|
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 =
|
|
8201
|
+
const here = path9.dirname(fileURLToPath2(import.meta.url));
|
|
7949
8202
|
const candidates = [
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
9196
|
+
description: {
|
|
8944
9197
|
type: "string",
|
|
8945
|
-
description: "
|
|
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
|
-
|
|
9200
|
+
role: {
|
|
8948
9201
|
type: "string",
|
|
8949
|
-
description: "
|
|
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 `
|
|
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(
|
|
9500
|
-
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
10183
|
+
return path10.join(os2.tmpdir(), `wssc-v${SESSION_CATALOG_PROTOCOL_VERSION}`, `${key}.sock`);
|
|
9920
10184
|
}
|
|
9921
10185
|
function sessionCatalogProjectServerMetadataPath(projectDir) {
|
|
9922
|
-
return
|
|
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 =
|
|
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
|
|
10738
|
+
import * as path12 from "node:path";
|
|
10475
10739
|
function configHistoryDir(globalRoot) {
|
|
10476
|
-
return
|
|
10740
|
+
return path12.join(globalRoot, "config-history");
|
|
10477
10741
|
}
|
|
10478
10742
|
function configSlug(absolutePath, globalRoot) {
|
|
10479
|
-
const rel =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
11648
|
-
|
|
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
|
|
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 =
|
|
12302
|
+
const resolved = path13.isAbsolute(dir) ? path13.resolve(dir) : path13.resolve(this.projectRoot, dir);
|
|
11990
12303
|
if (!this.allowOutsideProjectRoot) {
|
|
11991
|
-
const root =
|
|
11992
|
-
const rel =
|
|
11993
|
-
if (rel.startsWith("..") ||
|
|
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,
|
|
12735
|
+
function walk(value, schema, path45, errors, depth) {
|
|
12423
12736
|
if (depth > MAX_SCHEMA_DEPTH) {
|
|
12424
12737
|
errors.push({
|
|
12425
|
-
path:
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
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, `${
|
|
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
|
|
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 =
|
|
12835
|
-
const rel =
|
|
12836
|
-
if (rel.startsWith("..") ||
|
|
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
|
|
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 ?
|
|
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
|
|
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 =
|
|
14825
|
-
return relative8 === "" || !relative8.startsWith("..") && !
|
|
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 ||
|
|
15158
|
+
if (!input || path16.isAbsolute(input)) return null;
|
|
14829
15159
|
const normalized = input.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
14830
|
-
const resolved =
|
|
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 =
|
|
14846
|
-
this.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 =
|
|
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 =
|
|
14881
|
-
if (
|
|
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 =
|
|
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 (
|
|
15313
|
+
if (path16.isAbsolute(entry.linkTarget)) {
|
|
14984
15314
|
throw new Error("absolute symlink target refused");
|
|
14985
15315
|
}
|
|
14986
|
-
const resolvedLink =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
15048
|
-
|
|
15049
|
-
`.${
|
|
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 =
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
|
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 =
|
|
15250
|
-
return relative8 === "" || !relative8.startsWith("..") && !
|
|
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 =
|
|
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 =
|
|
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 =
|
|
15674
|
+
const root = path18.resolve(projectRoot);
|
|
15336
15675
|
const shown = validation.staleFiles.slice(0, NOTICE_PATH_LIMIT).map((entry) => {
|
|
15337
|
-
const relative8 =
|
|
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
|
|
15701
|
+
import * as path20 from "node:path";
|
|
15363
15702
|
|
|
15364
15703
|
// src/storage/session-store/paths.ts
|
|
15365
|
-
import * as
|
|
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 ?
|
|
15709
|
+
return shardKey ? path19.join(storeDir, shardKey, "_manifest.json") : path19.join(storeDir, "_manifest.json");
|
|
15371
15710
|
}
|
|
15372
15711
|
function shardKeyForSessionId(id) {
|
|
15373
|
-
const dirName =
|
|
15712
|
+
const dirName = path19.dirname(id);
|
|
15374
15713
|
return dirName === "." ? "" : dirName;
|
|
15375
15714
|
}
|
|
15376
15715
|
async function ensureShardDir(storeDir, id) {
|
|
15377
|
-
const dirPath =
|
|
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 =
|
|
15389
|
-
const base =
|
|
15390
|
-
const sessDir =
|
|
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
|
|
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() &&
|
|
15471
|
-
files.push({ id: sessionIdForFile(prefix, entry.name), filePath:
|
|
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(
|
|
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() &&
|
|
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(
|
|
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
|
|
15597
|
-
let boundary =
|
|
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 <
|
|
15602
|
-
const event =
|
|
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 =
|
|
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:
|
|
15619
|
-
model:
|
|
15620
|
-
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
|
-
|
|
15734
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
16389
|
+
import * as path22 from "node:path";
|
|
16022
16390
|
function isPrunableSessionJsonl(name) {
|
|
16023
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
16778
|
+
this.projectRoot = opts.projectRoot ? path23.resolve(opts.projectRoot) : void 0;
|
|
16411
16779
|
this.checkpointCas = this.projectRoot ? new SessionCheckpointCas({
|
|
16412
|
-
rootDir:
|
|
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:
|
|
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
|
|
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
|
-
|
|
17021
|
+
messages: [...carriedMessages, ...noticeMessages]
|
|
16638
17022
|
};
|
|
16639
17023
|
let handle;
|
|
16640
17024
|
try {
|
|
16641
|
-
handle = await
|
|
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:
|
|
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
|
|
16821
|
-
...criteria
|
|
17204
|
+
limit,
|
|
17205
|
+
...criteria
|
|
16822
17206
|
});
|
|
16823
|
-
return this.scrubSummaries(records)
|
|
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.
|
|
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
|
-
|
|
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 ?
|
|
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 ?
|
|
17784
|
+
dir = opts.sessionsRoot ? path24.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
17352
17785
|
} else if (opts.sessionsRoot) {
|
|
17353
|
-
dir =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
23201
|
+
const here = path26.dirname(fileURLToPath4(import.meta.url));
|
|
22766
23202
|
const candidates = [
|
|
22767
|
-
|
|
22768
|
-
|
|
22769
|
-
|
|
22770
|
-
|
|
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
|
|
23245
|
+
import * as path27 from "node:path";
|
|
22810
23246
|
var DESIGN_DIR = ".design";
|
|
22811
23247
|
function designProjectDir(projectRoot) {
|
|
22812
|
-
return
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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(
|
|
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(
|
|
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 (
|
|
27879
|
-
const absolute =
|
|
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 =
|
|
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 =
|
|
28391
|
+
const parent = path31.dirname(probe);
|
|
27891
28392
|
if (parent === probe) return absolute;
|
|
27892
|
-
missingSegments.unshift(
|
|
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 =
|
|
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 =
|
|
27920
|
-
return relative8.startsWith("../") ||
|
|
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
|
|
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 =
|
|
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 =
|
|
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" ?
|
|
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
|
|
30051
|
+
import * as path35 from "node:path";
|
|
29549
30052
|
|
|
29550
30053
|
// src/types/mode-prompts.ts
|
|
29551
|
-
import { readFileSync as
|
|
29552
|
-
import * as
|
|
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
|
|
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 =
|
|
30067
|
+
const here = path34.dirname(fileURLToPath5(import.meta.url));
|
|
29565
30068
|
const candidates = [
|
|
29566
|
-
|
|
29567
|
-
|
|
29568
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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 ===
|
|
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:
|
|
31325
|
+
const { readFileSync: readFileSync11 } = await import("node:fs");
|
|
30823
31326
|
server = createServer(
|
|
30824
|
-
{ cert:
|
|
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}${
|
|
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
|
|
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,
|
|
31168
|
-
diagnostics.push({ severity: "error", code, path:
|
|
31670
|
+
function error(diagnostics, code, path45, message) {
|
|
31671
|
+
diagnostics.push({ severity: "error", code, path: path45, message });
|
|
31169
31672
|
}
|
|
31170
|
-
function validatePatterns(value,
|
|
31673
|
+
function validatePatterns(value, path45, diagnostics) {
|
|
31171
31674
|
if (!Array.isArray(value)) {
|
|
31172
|
-
error(diagnostics, "invalid_pattern_list",
|
|
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
|
-
|
|
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 = `${
|
|
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
|
|
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 =
|
|
31352
|
-
const relative8 =
|
|
31353
|
-
return !!relative8 && !relative8.startsWith("..") && !
|
|
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
|
|
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) :
|
|
32190
|
+
return tail.length === 0 ? realpathSync(probe) : path38.join(realpathSync(probe), ...tail);
|
|
31688
32191
|
} catch {
|
|
31689
|
-
const parent =
|
|
32192
|
+
const parent = path38.dirname(probe);
|
|
31690
32193
|
if (parent === probe) return p;
|
|
31691
|
-
tail.unshift(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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,
|
|
33598
|
-
const segments =
|
|
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:
|
|
33656
|
-
).map(({ path:
|
|
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:
|
|
33664
|
-
).map(({ path:
|
|
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:
|
|
33693
|
-
if (deleteNestedPath(out,
|
|
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
|
|
34397
|
+
import * as path42 from "node:path";
|
|
33883
34398
|
function samePath(a, b) {
|
|
33884
|
-
let ra =
|
|
33885
|
-
let rb =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
35715
|
+
await ensureDir(path44.dirname(this.file));
|
|
35201
35716
|
const lock = {
|
|
35202
35717
|
v: 1,
|
|
35203
35718
|
sessionId,
|