@wrongstack/core 0.303.0 → 0.305.1
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/dist/chronicle/project-server.js +18 -48
- package/dist/coordination/agents/index.js +530 -130
- package/dist/coordination/agents/project-agent-consolidation.d.ts +5 -0
- package/dist/coordination/agents/project-agent-directive-outcome.d.ts +57 -0
- package/dist/coordination/agents/project-agent-identity.d.ts +26 -12
- package/dist/coordination/agents/project-agent-learning-policy.d.ts +22 -1
- package/dist/coordination/agents/project-agent-learning-structured.d.ts +46 -1
- package/dist/coordination/agents/project-agent-quarantine.d.ts +63 -0
- package/dist/coordination/agents/project-agent-skill-layer.d.ts +55 -10
- 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 +2 -1
- package/dist/coordination/index.js +1581 -955
- package/dist/coordination/mailbox-project-server.js +28 -57
- package/dist/core/agent-types.d.ts +4 -2
- package/dist/core/agent.d.ts +1 -0
- package/dist/core/context.d.ts +15 -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 +308 -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 +910 -693
- package/dist/execution/council-orchestrator.d.ts +3 -13
- package/dist/execution/index.js +211 -75
- package/dist/execution/one-shot-llm.d.ts +5 -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 +3505 -2539
- package/dist/infrastructure/index.js +247 -122
- package/dist/plugin/index.js +101 -3
- 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 +25 -20
- package/dist/security/secret-vault.d.ts +2 -0
- package/dist/session-catalog/index.js +62 -8
- package/dist/session-catalog/project-server.js +109 -78
- package/dist/session-catalog/protocol.d.ts +11 -4
- package/dist/session-catalog/store.d.ts +2 -2
- package/dist/storage/index.js +224 -67
- package/dist/storage/memory-consolidator.d.ts +4 -2
- 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/tools/index.js +174 -74
- package/dist/types/config/mcp-features.d.ts +31 -1
- package/dist/types/config/root.d.ts +12 -0
- package/dist/types/config/tools.d.ts +22 -0
- package/dist/types/config/ui.d.ts +7 -4
- package/dist/types/default-config.d.ts +1 -0
- package/dist/types/index.js +24 -1
- package/dist/types/session.d.ts +9 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +214 -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 +10 -13
- package/instructions/system-pro.md +18 -25
- package/instructions/system.md +18 -23
- package/package.json +3 -3
- package/skills/wrongstack-kanban/SKILL.md +95 -124
package/dist/defaults/index.js
CHANGED
|
@@ -1733,6 +1733,12 @@ function normalizeForComparison(text) {
|
|
|
1733
1733
|
}
|
|
1734
1734
|
|
|
1735
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
|
+
}
|
|
1736
1742
|
function parseLearnedEntryStamp(entry) {
|
|
1737
1743
|
const structuredMatch = entry.match(
|
|
1738
1744
|
/<!--\s*learned-stamp:\s*category=([\w-]+);\s*capturedAt=([^;]+?)\s*-->/
|
|
@@ -1851,6 +1857,10 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
|
|
|
1851
1857
|
const category = parseLearnedCategory(attributes["category"]) ?? "fact";
|
|
1852
1858
|
const capturedAt = attributes["capturedAt"] ?? "";
|
|
1853
1859
|
const skill = attributes["skill"];
|
|
1860
|
+
const trials = directiveTrials({
|
|
1861
|
+
applied: Number(attributes["applied"]),
|
|
1862
|
+
wins: Number(attributes["wins"])
|
|
1863
|
+
});
|
|
1854
1864
|
const start = (stamp.index ?? 0) + stamp[0].length;
|
|
1855
1865
|
const end = stamps[index + 1]?.index ?? raw.length;
|
|
1856
1866
|
const parsed = parseEntryBody(raw.slice(start, end));
|
|
@@ -1862,7 +1872,8 @@ function parseStructuredLearnedEntriesFromContent(raw, legacyEntries = splitLear
|
|
|
1862
1872
|
why: parsed.why || WHY_BY_CATEGORY[category],
|
|
1863
1873
|
how: parsed.how,
|
|
1864
1874
|
capturedAt,
|
|
1865
|
-
...skill ? { skill } : {}
|
|
1875
|
+
...skill ? { skill } : {},
|
|
1876
|
+
...trials.applied > 0 ? { applied: trials.applied, wins: trials.wins } : {}
|
|
1866
1877
|
});
|
|
1867
1878
|
}
|
|
1868
1879
|
if (structured.length === 0) {
|
|
@@ -1992,7 +2003,8 @@ function loadProjectAgentLearningPolicy(role, projectRoot) {
|
|
|
1992
2003
|
enabled: parsed.enabled !== false,
|
|
1993
2004
|
lifetimeCaptureCount: typeof parsed.lifetimeCaptureCount === "number" && Number.isInteger(parsed.lifetimeCaptureCount) && parsed.lifetimeCaptureCount >= 0 ? parsed.lifetimeCaptureCount : 0,
|
|
1994
2005
|
...typeof parsed.lastCaptureAt === "string" ? { lastCaptureAt: parsed.lastCaptureAt } : {},
|
|
1995
|
-
...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 } : {}
|
|
1996
2008
|
};
|
|
1997
2009
|
} catch {
|
|
1998
2010
|
return { ...DEFAULT_LEARNING_POLICY };
|
|
@@ -2407,6 +2419,8 @@ The file below stores **learning data for this project's "${role}" agent** \u201
|
|
|
2407
2419
|
- **Self-contained** \u2014 understandable without the surrounding session context.
|
|
2408
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.
|
|
2409
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
|
+
|
|
2410
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.
|
|
2411
2425
|
|
|
2412
2426
|
**Bad** (session log \u2014 rejected at capture time):
|
|
@@ -2691,8 +2705,16 @@ var TOOLS = {
|
|
|
2691
2705
|
"test",
|
|
2692
2706
|
"mailbox"
|
|
2693
2707
|
],
|
|
2694
|
-
/**
|
|
2695
|
-
|
|
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"],
|
|
2696
2718
|
/** Dependency management + CVE audit. */
|
|
2697
2719
|
deps: ["read", "grep", "glob", "install", "outdated", "audit", "json", "mailbox"],
|
|
2698
2720
|
/** Documentation authoring. */
|
|
@@ -5303,6 +5325,14 @@ var SHADOW_AGENT = {
|
|
|
5303
5325
|
};
|
|
5304
5326
|
var CRITIC_AGENT = defineAgent("critic", "Critic");
|
|
5305
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
|
+
}
|
|
5306
5336
|
var FLEET_ROSTER = {
|
|
5307
5337
|
"audit-log": AUDIT_LOG_AGENT,
|
|
5308
5338
|
"bug-hunter": BUG_HUNTER_AGENT,
|
|
@@ -5312,7 +5342,7 @@ var FLEET_ROSTER = {
|
|
|
5312
5342
|
generic: GENERIC_AGENT,
|
|
5313
5343
|
"shadow-agent": SHADOW_AGENT,
|
|
5314
5344
|
...Object.fromEntries(
|
|
5315
|
-
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, d
|
|
5345
|
+
ALL_AGENT_DEFINITIONS.map((d) => [d.config.role, withDispatchMetadata(d)])
|
|
5316
5346
|
)
|
|
5317
5347
|
};
|
|
5318
5348
|
var DEFAULT_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
@@ -7952,9 +7982,6 @@ import {
|
|
|
7952
7982
|
updateTaskAssignment
|
|
7953
7983
|
} from "@wrongstack/kanban";
|
|
7954
7984
|
|
|
7955
|
-
// src/coordination/director-kanban-queue-helpers.ts
|
|
7956
|
-
import { describeKanbanBoundary } from "@wrongstack/kanban";
|
|
7957
|
-
|
|
7958
7985
|
// src/coordination/director-input-helpers.ts
|
|
7959
7986
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
7960
7987
|
function stringArray2(value) {
|
|
@@ -7975,6 +8002,7 @@ function instantiateRosterConfig2(role, base) {
|
|
|
7975
8002
|
}
|
|
7976
8003
|
|
|
7977
8004
|
// src/coordination/director-kanban-queue-helpers.ts
|
|
8005
|
+
import { describeKanbanBoundary } from "@wrongstack/kanban";
|
|
7978
8006
|
function normalizeKanbanQueueInput(input) {
|
|
7979
8007
|
const raw = input ?? {};
|
|
7980
8008
|
return {
|
|
@@ -8309,378 +8337,336 @@ function makeLLMClassifier(complete) {
|
|
|
8309
8337
|
};
|
|
8310
8338
|
}
|
|
8311
8339
|
|
|
8312
|
-
// src/coordination/director-
|
|
8340
|
+
// src/coordination/director-basic-tools.ts
|
|
8313
8341
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
8314
|
-
function
|
|
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
|
+
};
|
|
8315
8361
|
return {
|
|
8316
|
-
name: "
|
|
8317
|
-
description: "
|
|
8318
|
-
usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
|
|
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.",
|
|
8319
8364
|
permission: "auto",
|
|
8320
8365
|
mutating: false,
|
|
8321
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],
|
|
8322
8389
|
inputSchema: {
|
|
8323
8390
|
type: "object",
|
|
8324
8391
|
properties: {
|
|
8325
|
-
|
|
8326
|
-
type: "string",
|
|
8327
|
-
description: "Original implementation task or acceptance goal being gated."
|
|
8328
|
-
},
|
|
8329
|
-
implementerTaskIds: {
|
|
8330
|
-
type: "array",
|
|
8331
|
-
items: { type: "string" },
|
|
8332
|
-
description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
|
|
8333
|
-
},
|
|
8334
|
-
repairSubagentId: {
|
|
8335
|
-
type: "string",
|
|
8336
|
-
description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
|
|
8337
|
-
},
|
|
8338
|
-
maxRepairAttempts: {
|
|
8339
|
-
type: "number",
|
|
8340
|
-
minimum: 0,
|
|
8341
|
-
maximum: 5,
|
|
8342
|
-
description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
|
|
8343
|
-
},
|
|
8344
|
-
targets: {
|
|
8345
|
-
type: "array",
|
|
8346
|
-
items: { type: "string" },
|
|
8347
|
-
description: "Files, packages, or paths that reviewer/verifier should focus on."
|
|
8348
|
-
},
|
|
8349
|
-
commands: {
|
|
8392
|
+
taskIds: {
|
|
8350
8393
|
type: "array",
|
|
8351
8394
|
items: { type: "string" },
|
|
8352
|
-
description:
|
|
8353
|
-
},
|
|
8354
|
-
expected: {
|
|
8355
|
-
type: "string",
|
|
8356
|
-
description: "Expected behavior or acceptance criteria."
|
|
8395
|
+
description: "One or more task ids returned by `assign_task`."
|
|
8357
8396
|
},
|
|
8358
|
-
|
|
8397
|
+
mode: {
|
|
8359
8398
|
type: "string",
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
reviewer: {
|
|
8363
|
-
type: "boolean",
|
|
8364
|
-
description: "Whether to run the reviewer lane. Default true."
|
|
8365
|
-
},
|
|
8366
|
-
verifier: {
|
|
8367
|
-
type: "boolean",
|
|
8368
|
-
description: "Whether to run the verifier lane. Default true."
|
|
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.'
|
|
8369
8401
|
},
|
|
8370
8402
|
timeoutMs: {
|
|
8371
8403
|
type: "number",
|
|
8372
8404
|
minimum: 1,
|
|
8373
|
-
description: "
|
|
8374
|
-
},
|
|
8375
|
-
reviewerWorktree: {
|
|
8376
|
-
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
8377
|
-
description: "Reviewer worktree override. Default off because reviewer is read-only."
|
|
8378
|
-
},
|
|
8379
|
-
verifierWorktree: {
|
|
8380
|
-
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
8381
|
-
description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
|
|
8405
|
+
description: 'mode:"any" only \u2014 return {timedOut:true, completed:[]} if nothing completes within this window instead of blocking.'
|
|
8382
8406
|
}
|
|
8383
|
-
}
|
|
8407
|
+
},
|
|
8408
|
+
required: ["taskIds"]
|
|
8384
8409
|
},
|
|
8385
8410
|
async execute(input) {
|
|
8386
|
-
const i =
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
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
|
+
);
|
|
8390
8417
|
return {
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
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
|
+
} : {}
|
|
8394
8425
|
};
|
|
8395
8426
|
}
|
|
8396
|
-
const
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
taskRoleById.set(taskId, "verifier");
|
|
8422
|
-
}
|
|
8423
|
-
if (runReviewer) {
|
|
8424
|
-
const subagentId = await director.spawn(
|
|
8425
|
-
makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
|
|
8426
|
-
);
|
|
8427
|
-
const taskId = await director.assign({
|
|
8428
|
-
id: randomUUID6(),
|
|
8429
|
-
subagentId,
|
|
8430
|
-
description: buildReviewerTask(i, {
|
|
8431
|
-
attempt,
|
|
8432
|
-
implementerResults,
|
|
8433
|
-
repairResults,
|
|
8434
|
-
priorAttempts: attempts
|
|
8435
|
-
}),
|
|
8436
|
-
timeoutMs: i.timeoutMs
|
|
8437
|
-
});
|
|
8438
|
-
gateTaskIds.push(taskId);
|
|
8439
|
-
taskRoleById.set(taskId, "reviewer");
|
|
8440
|
-
}
|
|
8441
|
-
const gateResults = await director.awaitTasks(gateTaskIds);
|
|
8442
|
-
const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
|
|
8443
|
-
const assessment = assessQualityGate(reports);
|
|
8444
|
-
attempts.push({ attempt, reports, ...assessment });
|
|
8445
|
-
if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
|
|
8446
|
-
return {
|
|
8447
|
-
verdict: assessment.verdict,
|
|
8448
|
-
passed: assessment.passed,
|
|
8449
|
-
attempts,
|
|
8450
|
-
repairAttemptsUsed: repairResults.length,
|
|
8451
|
-
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
8452
|
-
nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
|
|
8453
|
-
};
|
|
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)."
|
|
8454
8452
|
}
|
|
8455
|
-
|
|
8456
|
-
|
|
8457
|
-
|
|
8458
|
-
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
const
|
|
8462
|
-
|
|
8463
|
-
if (
|
|
8464
|
-
return {
|
|
8465
|
-
verdict: "fail",
|
|
8466
|
-
passed: false,
|
|
8467
|
-
attempts,
|
|
8468
|
-
repairAttemptsUsed: repairResults.length,
|
|
8469
|
-
repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
|
|
8470
|
-
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
8471
|
-
nextAction: "repair_failed"
|
|
8472
|
-
};
|
|
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 };
|
|
8473
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) };
|
|
8474
8472
|
}
|
|
8475
8473
|
}
|
|
8476
8474
|
};
|
|
8477
8475
|
}
|
|
8478
|
-
function
|
|
8479
|
-
const raw = input ?? {};
|
|
8476
|
+
function makeAskResultTool(director) {
|
|
8480
8477
|
return {
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
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
|
+
}
|
|
8494
8505
|
};
|
|
8495
8506
|
}
|
|
8496
|
-
function
|
|
8497
|
-
if (!Number.isFinite(value)) return 0;
|
|
8498
|
-
return Math.max(0, Math.min(5, Math.floor(value)));
|
|
8499
|
-
}
|
|
8500
|
-
function makeQualityGateSubagentConfig(role, roster, worktree) {
|
|
8501
|
-
const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
|
|
8507
|
+
function makeRollUpTool(director) {
|
|
8502
8508
|
return {
|
|
8503
|
-
|
|
8504
|
-
|
|
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
|
+
}
|
|
8505
8535
|
};
|
|
8506
8536
|
}
|
|
8507
|
-
function
|
|
8508
|
-
return [
|
|
8509
|
-
"Run the independent verification gate for this implementation.",
|
|
8510
|
-
"Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
|
|
8511
|
-
"Do not edit code. Run the smallest meaningful command set and include exact failures.",
|
|
8512
|
-
"",
|
|
8513
|
-
`Gate attempt: ${state.attempt}`,
|
|
8514
|
-
input.task ? `Original task:
|
|
8515
|
-
${input.task}` : void 0,
|
|
8516
|
-
input.targets?.length ? `Targets:
|
|
8517
|
-
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
8518
|
-
input.commands?.length ? `Required or suggested commands:
|
|
8519
|
-
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
8520
|
-
input.expected ? `Expected behavior:
|
|
8521
|
-
${input.expected}` : void 0,
|
|
8522
|
-
input.evidence ? `Known evidence:
|
|
8523
|
-
${input.evidence}` : void 0,
|
|
8524
|
-
taskResultsBlock("Implementer results", state.implementerResults),
|
|
8525
|
-
taskResultsBlock("Repair results so far", state.repairResults),
|
|
8526
|
-
priorAttemptsBlock(state.priorAttempts)
|
|
8527
|
-
].filter((part) => !!part).join("\n\n");
|
|
8528
|
-
}
|
|
8529
|
-
function buildReviewerTask(input, state) {
|
|
8530
|
-
return [
|
|
8531
|
-
"Run independent code review for this implementation.",
|
|
8532
|
-
"Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
|
|
8533
|
-
"Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
|
|
8534
|
-
"",
|
|
8535
|
-
`Gate attempt: ${state.attempt}`,
|
|
8536
|
-
input.task ? `Original task:
|
|
8537
|
-
${input.task}` : void 0,
|
|
8538
|
-
input.targets?.length ? `Targets:
|
|
8539
|
-
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
8540
|
-
input.expected ? `Expected behavior:
|
|
8541
|
-
${input.expected}` : void 0,
|
|
8542
|
-
input.evidence ? `Known evidence:
|
|
8543
|
-
${input.evidence}` : void 0,
|
|
8544
|
-
taskResultsBlock("Implementer results", state.implementerResults),
|
|
8545
|
-
taskResultsBlock("Repair results so far", state.repairResults),
|
|
8546
|
-
priorAttemptsBlock(state.priorAttempts)
|
|
8547
|
-
].filter((part) => !!part).join("\n\n");
|
|
8548
|
-
}
|
|
8549
|
-
function buildRepairTask(input, attempt, attemptNumber) {
|
|
8550
|
-
return [
|
|
8551
|
-
`Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
|
|
8552
|
-
"Address every must-fix item. Run relevant checks before returning.",
|
|
8553
|
-
"Do not claim done unless verifier/reviewer feedback is resolved.",
|
|
8554
|
-
"",
|
|
8555
|
-
input.task ? `Original task:
|
|
8556
|
-
${input.task}` : void 0,
|
|
8557
|
-
input.targets?.length ? `Targets:
|
|
8558
|
-
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
8559
|
-
input.commands?.length ? `Commands expected to pass:
|
|
8560
|
-
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
8561
|
-
input.expected ? `Expected behavior:
|
|
8562
|
-
${input.expected}` : void 0,
|
|
8563
|
-
attempt.mustFix.length ? `Must fix:
|
|
8564
|
-
${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
8565
|
-
attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
|
|
8566
|
-
${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
8567
|
-
`Reviewer/verifier reports:
|
|
8568
|
-
${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
|
|
8569
|
-
${r.summary}`).join("\n\n")}`
|
|
8570
|
-
].filter((part) => !!part).join("\n\n");
|
|
8571
|
-
}
|
|
8572
|
-
function taskResultsBlock(title, results) {
|
|
8573
|
-
if (results.length === 0) return void 0;
|
|
8574
|
-
return `${title}:
|
|
8575
|
-
${results.map((r) => `### ${r.subagentId}/${r.taskId}
|
|
8576
|
-
${summarizeTaskResult(r).summary}`).join("\n\n")}`;
|
|
8577
|
-
}
|
|
8578
|
-
function priorAttemptsBlock(attempts) {
|
|
8579
|
-
if (attempts.length === 0) return void 0;
|
|
8580
|
-
return `Prior quality gate attempts:
|
|
8581
|
-
${attempts.map(
|
|
8582
|
-
(a) => `### Attempt ${a.attempt}
|
|
8583
|
-
${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
|
|
8584
|
-
).join("\n\n")}`;
|
|
8585
|
-
}
|
|
8586
|
-
function summarizeTaskResult(result) {
|
|
8587
|
-
const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
|
|
8588
|
-
const error2 = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
|
|
8537
|
+
function makeTerminateTool(director) {
|
|
8589
8538
|
return {
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
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
|
+
}
|
|
8595
8554
|
};
|
|
8596
8555
|
}
|
|
8597
|
-
function
|
|
8598
|
-
const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
|
|
8599
|
-
const summary = summarizeTaskResult(result);
|
|
8600
|
-
const text = summary.summary;
|
|
8601
|
-
const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
|
|
8602
|
-
if (result.status !== "success") {
|
|
8603
|
-
return {
|
|
8604
|
-
role: resolvedRole,
|
|
8605
|
-
subagentId: result.subagentId,
|
|
8606
|
-
taskId: result.taskId,
|
|
8607
|
-
status: result.status,
|
|
8608
|
-
verdict: "fail",
|
|
8609
|
-
summary: text,
|
|
8610
|
-
uncertaintyFlags,
|
|
8611
|
-
error: summary.error ?? result.status
|
|
8612
|
-
};
|
|
8613
|
-
}
|
|
8556
|
+
function makeTerminateAllTool(director) {
|
|
8614
8557
|
return {
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
let hasFail = false;
|
|
8628
|
-
let hasInconclusive = false;
|
|
8629
|
-
for (const report of reports) {
|
|
8630
|
-
if (report.verdict === "fail") hasFail = true;
|
|
8631
|
-
if (report.verdict === "inconclusive") hasInconclusive = true;
|
|
8632
|
-
const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
|
|
8633
|
-
if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
|
|
8634
|
-
if (report.uncertaintyFlags) {
|
|
8635
|
-
uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
|
|
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
|
+
};
|
|
8636
8570
|
}
|
|
8637
|
-
|
|
8638
|
-
}
|
|
8639
|
-
if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
|
|
8640
|
-
if (hasInconclusive || reports.length === 0) {
|
|
8641
|
-
return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
|
|
8642
|
-
}
|
|
8643
|
-
return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
|
|
8571
|
+
};
|
|
8644
8572
|
}
|
|
8645
|
-
function
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
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
|
+
}
|
|
8657
8668
|
}
|
|
8658
|
-
|
|
8659
|
-
}
|
|
8660
|
-
if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
|
|
8661
|
-
if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
|
|
8662
|
-
if (sectionHasBlockingContent(text, "Failures")) return "fail";
|
|
8663
|
-
return "inconclusive";
|
|
8664
|
-
}
|
|
8665
|
-
function sectionHasBlockingContent(text, heading) {
|
|
8666
|
-
const section = extractSection(text, heading);
|
|
8667
|
-
if (!section) return false;
|
|
8668
|
-
return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
|
|
8669
|
-
}
|
|
8670
|
-
function extractSection(text, heading) {
|
|
8671
|
-
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8672
|
-
const pattern = new RegExp(
|
|
8673
|
-
`(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
|
|
8674
|
-
"i"
|
|
8675
|
-
);
|
|
8676
|
-
const match = text.match(pattern);
|
|
8677
|
-
const body = match?.[1]?.trim();
|
|
8678
|
-
return body ? body : void 0;
|
|
8679
|
-
}
|
|
8680
|
-
function excerpt(text, max) {
|
|
8681
|
-
if (text.length <= max) return text;
|
|
8682
|
-
return `${text.slice(0, max - 20).trimEnd()}
|
|
8683
|
-
...(truncated)`;
|
|
8669
|
+
};
|
|
8684
8670
|
}
|
|
8685
8671
|
|
|
8686
8672
|
// src/coordination/director-collab-tools.ts
|
|
@@ -8801,336 +8787,378 @@ function makeWorkCompleteTool(director) {
|
|
|
8801
8787
|
};
|
|
8802
8788
|
}
|
|
8803
8789
|
|
|
8804
|
-
// src/coordination/director-
|
|
8790
|
+
// src/coordination/director-quality-gate-tool.ts
|
|
8805
8791
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
8806
|
-
function
|
|
8807
|
-
const inputSchema = {
|
|
8808
|
-
type: "object",
|
|
8809
|
-
properties: {
|
|
8810
|
-
subagentId: { type: "string", minLength: 1, description: "Target subagent id. Required." },
|
|
8811
|
-
description: {
|
|
8812
|
-
type: "string",
|
|
8813
|
-
minLength: 1,
|
|
8814
|
-
description: "The task in natural language \u2014 what you want this subagent to do."
|
|
8815
|
-
},
|
|
8816
|
-
maxToolCalls: {
|
|
8817
|
-
type: "number",
|
|
8818
|
-
minimum: 1,
|
|
8819
|
-
description: "Optional per-task tool-call budget override."
|
|
8820
|
-
},
|
|
8821
|
-
timeoutMs: { type: "number", minimum: 1, description: "Optional per-task timeout in ms." }
|
|
8822
|
-
},
|
|
8823
|
-
required: ["subagentId", "description"]
|
|
8824
|
-
};
|
|
8792
|
+
function makeQualityGateTool(director, roster) {
|
|
8825
8793
|
return {
|
|
8826
|
-
name: "
|
|
8827
|
-
description: "
|
|
8794
|
+
name: "quality_gate",
|
|
8795
|
+
description: "Run a first-class implementation quality gate. It can await implementer task ids, spawn independent verifier/reviewer agents, summarize their verdicts, and optionally send must-fix feedback back to an implementer until the gate passes or the repair-attempt limit is reached.",
|
|
8796
|
+
usageHint: "Use after code-changing work. Provide implementerTaskIds when available. Add repairSubagentId to iterate fixes automatically. Verdict only passes when every enabled reviewer/verifier explicitly passes.",
|
|
8828
8797
|
permission: "auto",
|
|
8829
8798
|
mutating: false,
|
|
8830
8799
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
8831
|
-
inputSchema,
|
|
8832
|
-
async execute(input) {
|
|
8833
|
-
const i = input;
|
|
8834
|
-
const task = {
|
|
8835
|
-
id: randomUUID7(),
|
|
8836
|
-
description: i.description,
|
|
8837
|
-
subagentId: i.subagentId,
|
|
8838
|
-
maxToolCalls: i.maxToolCalls,
|
|
8839
|
-
timeoutMs: i.timeoutMs
|
|
8840
|
-
};
|
|
8841
|
-
const taskId = await director.assign(task);
|
|
8842
|
-
return { taskId, subagentId: i.subagentId };
|
|
8843
|
-
}
|
|
8844
|
-
};
|
|
8845
|
-
}
|
|
8846
|
-
function makeAwaitTasksTool(director) {
|
|
8847
|
-
return {
|
|
8848
|
-
name: "await_tasks",
|
|
8849
|
-
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.',
|
|
8850
|
-
permission: "auto",
|
|
8851
|
-
mutating: false,
|
|
8852
|
-
capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
|
|
8853
8800
|
inputSchema: {
|
|
8854
8801
|
type: "object",
|
|
8855
8802
|
properties: {
|
|
8856
|
-
|
|
8803
|
+
task: {
|
|
8804
|
+
type: "string",
|
|
8805
|
+
description: "Original implementation task or acceptance goal being gated."
|
|
8806
|
+
},
|
|
8807
|
+
implementerTaskIds: {
|
|
8857
8808
|
type: "array",
|
|
8858
8809
|
items: { type: "string" },
|
|
8859
|
-
description: "
|
|
8810
|
+
description: "Optional completed or in-flight implementer task ids to await and include as implementation evidence."
|
|
8860
8811
|
},
|
|
8861
|
-
|
|
8812
|
+
repairSubagentId: {
|
|
8862
8813
|
type: "string",
|
|
8863
|
-
|
|
8864
|
-
|
|
8814
|
+
description: "Optional implementer subagent id. When set and the gate fails, quality_gate assigns a repair task with reviewer/verifier feedback and reruns the gate."
|
|
8815
|
+
},
|
|
8816
|
+
maxRepairAttempts: {
|
|
8817
|
+
type: "number",
|
|
8818
|
+
minimum: 0,
|
|
8819
|
+
maximum: 5,
|
|
8820
|
+
description: "Maximum automatic repair iterations. Default: 2 when repairSubagentId is set, otherwise 0."
|
|
8821
|
+
},
|
|
8822
|
+
targets: {
|
|
8823
|
+
type: "array",
|
|
8824
|
+
items: { type: "string" },
|
|
8825
|
+
description: "Files, packages, or paths that reviewer/verifier should focus on."
|
|
8826
|
+
},
|
|
8827
|
+
commands: {
|
|
8828
|
+
type: "array",
|
|
8829
|
+
items: { type: "string" },
|
|
8830
|
+
description: 'Verification commands that should pass, e.g. ["pnpm --filter @wrongstack/core typecheck"].'
|
|
8831
|
+
},
|
|
8832
|
+
expected: {
|
|
8833
|
+
type: "string",
|
|
8834
|
+
description: "Expected behavior or acceptance criteria."
|
|
8835
|
+
},
|
|
8836
|
+
evidence: {
|
|
8837
|
+
type: "string",
|
|
8838
|
+
description: "Known implementation notes, diff summary, or commands already run."
|
|
8839
|
+
},
|
|
8840
|
+
reviewer: {
|
|
8841
|
+
type: "boolean",
|
|
8842
|
+
description: "Whether to run the reviewer lane. Default true."
|
|
8843
|
+
},
|
|
8844
|
+
verifier: {
|
|
8845
|
+
type: "boolean",
|
|
8846
|
+
description: "Whether to run the verifier lane. Default true."
|
|
8865
8847
|
},
|
|
8866
8848
|
timeoutMs: {
|
|
8867
8849
|
type: "number",
|
|
8868
8850
|
minimum: 1,
|
|
8869
|
-
description:
|
|
8851
|
+
description: "Optional per reviewer/verifier/repair task timeout."
|
|
8852
|
+
},
|
|
8853
|
+
reviewerWorktree: {
|
|
8854
|
+
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
8855
|
+
description: "Reviewer worktree override. Default off because reviewer is read-only."
|
|
8856
|
+
},
|
|
8857
|
+
verifierWorktree: {
|
|
8858
|
+
anyOf: [{ type: "boolean" }, { type: "string", enum: ["auto", "required", "off"] }],
|
|
8859
|
+
description: "Verifier worktree override. Default auto so test artifacts stay isolated when fleet policy wants it."
|
|
8870
8860
|
}
|
|
8871
|
-
}
|
|
8872
|
-
required: ["taskIds"]
|
|
8861
|
+
}
|
|
8873
8862
|
},
|
|
8874
8863
|
async execute(input) {
|
|
8875
|
-
const i = input;
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
i.timeoutMs !== void 0 ? { timeoutMs: i.timeoutMs } : void 0
|
|
8880
|
-
);
|
|
8864
|
+
const i = normalizeQualityGateInput(input);
|
|
8865
|
+
const runReviewer = i.reviewer !== false;
|
|
8866
|
+
const runVerifier = i.verifier !== false;
|
|
8867
|
+
if (!runReviewer && !runVerifier) {
|
|
8881
8868
|
return {
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
...r.timedOut ? { timedOut: true } : {},
|
|
8886
|
-
...r.pending.length > 0 ? {
|
|
8887
|
-
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.'
|
|
8888
|
-
} : {}
|
|
8869
|
+
verdict: "inconclusive",
|
|
8870
|
+
passed: false,
|
|
8871
|
+
error: "quality_gate requires reviewer, verifier, or both."
|
|
8889
8872
|
};
|
|
8890
8873
|
}
|
|
8891
|
-
const
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
8904
|
-
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8874
|
+
const implementerResults = i.implementerTaskIds && i.implementerTaskIds.length > 0 ? await director.awaitTasks(i.implementerTaskIds) : [];
|
|
8875
|
+
const maxRepairAttempts = clampRepairAttempts(
|
|
8876
|
+
i.maxRepairAttempts ?? (i.repairSubagentId ? 2 : 0)
|
|
8877
|
+
);
|
|
8878
|
+
const repairResults = [];
|
|
8879
|
+
const attempts = [];
|
|
8880
|
+
for (let attempt = 1; ; attempt++) {
|
|
8881
|
+
const gateTaskIds = [];
|
|
8882
|
+
const taskRoleById = /* @__PURE__ */ new Map();
|
|
8883
|
+
if (runVerifier) {
|
|
8884
|
+
const subagentId = await director.spawn(
|
|
8885
|
+
makeQualityGateSubagentConfig("verifier", roster, i.verifierWorktree ?? "auto")
|
|
8886
|
+
);
|
|
8887
|
+
const taskId = await director.assign({
|
|
8888
|
+
id: randomUUID7(),
|
|
8889
|
+
subagentId,
|
|
8890
|
+
description: buildVerifierTask(i, {
|
|
8891
|
+
attempt,
|
|
8892
|
+
implementerResults,
|
|
8893
|
+
repairResults,
|
|
8894
|
+
priorAttempts: attempts
|
|
8895
|
+
}),
|
|
8896
|
+
timeoutMs: i.timeoutMs
|
|
8897
|
+
});
|
|
8898
|
+
gateTaskIds.push(taskId);
|
|
8899
|
+
taskRoleById.set(taskId, "verifier");
|
|
8900
|
+
}
|
|
8901
|
+
if (runReviewer) {
|
|
8902
|
+
const subagentId = await director.spawn(
|
|
8903
|
+
makeQualityGateSubagentConfig("reviewer", roster, i.reviewerWorktree ?? "off")
|
|
8904
|
+
);
|
|
8905
|
+
const taskId = await director.assign({
|
|
8906
|
+
id: randomUUID7(),
|
|
8907
|
+
subagentId,
|
|
8908
|
+
description: buildReviewerTask(i, {
|
|
8909
|
+
attempt,
|
|
8910
|
+
implementerResults,
|
|
8911
|
+
repairResults,
|
|
8912
|
+
priorAttempts: attempts
|
|
8913
|
+
}),
|
|
8914
|
+
timeoutMs: i.timeoutMs
|
|
8915
|
+
});
|
|
8916
|
+
gateTaskIds.push(taskId);
|
|
8917
|
+
taskRoleById.set(taskId, "reviewer");
|
|
8916
8918
|
}
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8919
|
+
const gateResults = await director.awaitTasks(gateTaskIds);
|
|
8920
|
+
const reports = gateResults.map((r) => assessRoleResult(taskRoleById.get(r.taskId), r));
|
|
8921
|
+
const assessment = assessQualityGate(reports);
|
|
8922
|
+
attempts.push({ attempt, reports, ...assessment });
|
|
8923
|
+
if (assessment.passed || !i.repairSubagentId || attempt > maxRepairAttempts) {
|
|
8924
|
+
return {
|
|
8925
|
+
verdict: assessment.verdict,
|
|
8926
|
+
passed: assessment.passed,
|
|
8927
|
+
attempts,
|
|
8928
|
+
repairAttemptsUsed: repairResults.length,
|
|
8929
|
+
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
8930
|
+
nextAction: assessment.passed ? "accept" : i.repairSubagentId && attempt > maxRepairAttempts ? "manual_intervention_or_raise_repair_limit" : "inspect_failures"
|
|
8931
|
+
};
|
|
8932
|
+
}
|
|
8933
|
+
const repairTaskId = await director.assign({
|
|
8934
|
+
id: randomUUID7(),
|
|
8935
|
+
subagentId: i.repairSubagentId,
|
|
8936
|
+
description: buildRepairTask(i, attempts[attempts.length - 1], attempt),
|
|
8937
|
+
timeoutMs: i.timeoutMs
|
|
8938
|
+
});
|
|
8939
|
+
const [repairResult] = await director.awaitTasks([repairTaskId]);
|
|
8940
|
+
if (repairResult) repairResults.push(repairResult);
|
|
8941
|
+
if (repairResult?.status !== "success") {
|
|
8942
|
+
return {
|
|
8943
|
+
verdict: "fail",
|
|
8944
|
+
passed: false,
|
|
8945
|
+
attempts,
|
|
8946
|
+
repairAttemptsUsed: repairResults.length,
|
|
8947
|
+
repairResult: repairResult ? summarizeTaskResult(repairResult) : void 0,
|
|
8948
|
+
implementerResults: implementerResults.map(summarizeTaskResult),
|
|
8949
|
+
nextAction: "repair_failed"
|
|
8950
|
+
};
|
|
8927
8951
|
}
|
|
8928
|
-
return {
|
|
8929
|
-
ok: true,
|
|
8930
|
-
answer: stored.summary,
|
|
8931
|
-
_answerKey: stored.key,
|
|
8932
|
-
_hint: "Response was large and stored. Use ask_result with the key to retrieve it."
|
|
8933
|
-
};
|
|
8934
|
-
} catch (err) {
|
|
8935
|
-
return { ok: false, error: toErrorMessage(err) };
|
|
8936
8952
|
}
|
|
8937
8953
|
}
|
|
8938
8954
|
};
|
|
8939
8955
|
}
|
|
8940
|
-
function
|
|
8956
|
+
function normalizeQualityGateInput(input) {
|
|
8957
|
+
const raw = input ?? {};
|
|
8941
8958
|
return {
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
},
|
|
8956
|
-
required: ["key"]
|
|
8957
|
-
},
|
|
8958
|
-
async execute(input) {
|
|
8959
|
-
const i = input;
|
|
8960
|
-
const value = director.largeAnswerStore.retrieveAnswer(i.key);
|
|
8961
|
-
if (value === void 0) {
|
|
8962
|
-
return {
|
|
8963
|
-
ok: false,
|
|
8964
|
-
error: `No stored answer found for key "${i.key}" \u2014 it may have been cleared or the key is invalid.`
|
|
8965
|
-
};
|
|
8966
|
-
}
|
|
8967
|
-
return { ok: true, value };
|
|
8968
|
-
}
|
|
8959
|
+
task: typeof raw.task === "string" ? raw.task : void 0,
|
|
8960
|
+
implementerTaskIds: stringArray2(raw.implementerTaskIds),
|
|
8961
|
+
repairSubagentId: typeof raw.repairSubagentId === "string" && raw.repairSubagentId.trim() ? raw.repairSubagentId.trim() : void 0,
|
|
8962
|
+
maxRepairAttempts: typeof raw.maxRepairAttempts === "number" ? raw.maxRepairAttempts : void 0,
|
|
8963
|
+
targets: stringArray2(raw.targets),
|
|
8964
|
+
commands: stringArray2(raw.commands),
|
|
8965
|
+
expected: typeof raw.expected === "string" ? raw.expected : void 0,
|
|
8966
|
+
evidence: typeof raw.evidence === "string" ? raw.evidence : void 0,
|
|
8967
|
+
reviewer: typeof raw.reviewer === "boolean" ? raw.reviewer : void 0,
|
|
8968
|
+
verifier: typeof raw.verifier === "boolean" ? raw.verifier : void 0,
|
|
8969
|
+
timeoutMs: typeof raw.timeoutMs === "number" ? raw.timeoutMs : void 0,
|
|
8970
|
+
reviewerWorktree: normalizeWorktreeOverride(raw.reviewerWorktree),
|
|
8971
|
+
verifierWorktree: normalizeWorktreeOverride(raw.verifierWorktree)
|
|
8969
8972
|
};
|
|
8970
8973
|
}
|
|
8971
|
-
function
|
|
8974
|
+
function clampRepairAttempts(value) {
|
|
8975
|
+
if (!Number.isFinite(value)) return 0;
|
|
8976
|
+
return Math.max(0, Math.min(5, Math.floor(value)));
|
|
8977
|
+
}
|
|
8978
|
+
function makeQualityGateSubagentConfig(role, roster, worktree) {
|
|
8979
|
+
const base = roster?.[role] ?? getAgentDefinition(role)?.config ?? { name: role, role };
|
|
8972
8980
|
return {
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
permission: "auto",
|
|
8976
|
-
mutating: false,
|
|
8977
|
-
capabilities: [ToolCapabilities.COORDINATION_FLEET_READ],
|
|
8978
|
-
inputSchema: {
|
|
8979
|
-
type: "object",
|
|
8980
|
-
properties: {
|
|
8981
|
-
taskIds: {
|
|
8982
|
-
type: "array",
|
|
8983
|
-
items: { type: "string" },
|
|
8984
|
-
description: "Completed task ids to aggregate."
|
|
8985
|
-
},
|
|
8986
|
-
style: {
|
|
8987
|
-
type: "string",
|
|
8988
|
-
enum: ["markdown", "json"],
|
|
8989
|
-
description: "Output flavor \u2014 markdown (default) or json."
|
|
8990
|
-
}
|
|
8991
|
-
},
|
|
8992
|
-
required: ["taskIds"]
|
|
8993
|
-
},
|
|
8994
|
-
async execute(input) {
|
|
8995
|
-
const i = input;
|
|
8996
|
-
const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
|
|
8997
|
-
return { summary, count: i.taskIds.length };
|
|
8998
|
-
}
|
|
8981
|
+
...instantiateRosterConfig2(role, base),
|
|
8982
|
+
worktree
|
|
8999
8983
|
};
|
|
9000
8984
|
}
|
|
9001
|
-
function
|
|
8985
|
+
function buildVerifierTask(input, state) {
|
|
8986
|
+
return [
|
|
8987
|
+
"Run the independent verification gate for this implementation.",
|
|
8988
|
+
"Return Markdown with `## Verdict` and make the first verdict word exactly `pass`, `fail`, or `blocked`.",
|
|
8989
|
+
"Do not edit code. Run the smallest meaningful command set and include exact failures.",
|
|
8990
|
+
"",
|
|
8991
|
+
`Gate attempt: ${state.attempt}`,
|
|
8992
|
+
input.task ? `Original task:
|
|
8993
|
+
${input.task}` : void 0,
|
|
8994
|
+
input.targets?.length ? `Targets:
|
|
8995
|
+
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
8996
|
+
input.commands?.length ? `Required or suggested commands:
|
|
8997
|
+
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
8998
|
+
input.expected ? `Expected behavior:
|
|
8999
|
+
${input.expected}` : void 0,
|
|
9000
|
+
input.evidence ? `Known evidence:
|
|
9001
|
+
${input.evidence}` : void 0,
|
|
9002
|
+
taskResultsBlock("Implementer results", state.implementerResults),
|
|
9003
|
+
taskResultsBlock("Repair results so far", state.repairResults),
|
|
9004
|
+
priorAttemptsBlock(state.priorAttempts)
|
|
9005
|
+
].filter((part) => !!part).join("\n\n");
|
|
9006
|
+
}
|
|
9007
|
+
function buildReviewerTask(input, state) {
|
|
9008
|
+
return [
|
|
9009
|
+
"Run independent code review for this implementation.",
|
|
9010
|
+
"Return Markdown with `## Verdict` and make the first verdict phrase exactly `approve`, `request changes`, or `needs verification`.",
|
|
9011
|
+
"Do not edit code. Treat missing proof, vague tests, and uncertainty as blocking until verifier evidence exists.",
|
|
9012
|
+
"",
|
|
9013
|
+
`Gate attempt: ${state.attempt}`,
|
|
9014
|
+
input.task ? `Original task:
|
|
9015
|
+
${input.task}` : void 0,
|
|
9016
|
+
input.targets?.length ? `Targets:
|
|
9017
|
+
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
9018
|
+
input.expected ? `Expected behavior:
|
|
9019
|
+
${input.expected}` : void 0,
|
|
9020
|
+
input.evidence ? `Known evidence:
|
|
9021
|
+
${input.evidence}` : void 0,
|
|
9022
|
+
taskResultsBlock("Implementer results", state.implementerResults),
|
|
9023
|
+
taskResultsBlock("Repair results so far", state.repairResults),
|
|
9024
|
+
priorAttemptsBlock(state.priorAttempts)
|
|
9025
|
+
].filter((part) => !!part).join("\n\n");
|
|
9026
|
+
}
|
|
9027
|
+
function buildRepairTask(input, attempt, attemptNumber) {
|
|
9028
|
+
return [
|
|
9029
|
+
`Repair the implementation after quality gate attempt ${attemptNumber} failed.`,
|
|
9030
|
+
"Address every must-fix item. Run relevant checks before returning.",
|
|
9031
|
+
"Do not claim done unless verifier/reviewer feedback is resolved.",
|
|
9032
|
+
"",
|
|
9033
|
+
input.task ? `Original task:
|
|
9034
|
+
${input.task}` : void 0,
|
|
9035
|
+
input.targets?.length ? `Targets:
|
|
9036
|
+
${input.targets.map((t) => `- ${t}`).join("\n")}` : void 0,
|
|
9037
|
+
input.commands?.length ? `Commands expected to pass:
|
|
9038
|
+
${input.commands.map((c) => `- ${c}`).join("\n")}` : void 0,
|
|
9039
|
+
input.expected ? `Expected behavior:
|
|
9040
|
+
${input.expected}` : void 0,
|
|
9041
|
+
attempt.mustFix.length ? `Must fix:
|
|
9042
|
+
${attempt.mustFix.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
9043
|
+
attempt.uncertaintyFlags.length ? `Uncertainty flags to resolve:
|
|
9044
|
+
${attempt.uncertaintyFlags.map((f) => `- ${f}`).join("\n")}` : void 0,
|
|
9045
|
+
`Reviewer/verifier reports:
|
|
9046
|
+
${attempt.reports.map((r) => `### ${r.role} (${r.verdict})
|
|
9047
|
+
${r.summary}`).join("\n\n")}`
|
|
9048
|
+
].filter((part) => !!part).join("\n\n");
|
|
9049
|
+
}
|
|
9050
|
+
function taskResultsBlock(title, results) {
|
|
9051
|
+
if (results.length === 0) return void 0;
|
|
9052
|
+
return `${title}:
|
|
9053
|
+
${results.map((r) => `### ${r.subagentId}/${r.taskId}
|
|
9054
|
+
${summarizeTaskResult(r).summary}`).join("\n\n")}`;
|
|
9055
|
+
}
|
|
9056
|
+
function priorAttemptsBlock(attempts) {
|
|
9057
|
+
if (attempts.length === 0) return void 0;
|
|
9058
|
+
return `Prior quality gate attempts:
|
|
9059
|
+
${attempts.map(
|
|
9060
|
+
(a) => `### Attempt ${a.attempt}
|
|
9061
|
+
${a.reports.map((r) => `- ${r.role}: ${r.verdict}${r.error ? ` (${r.error})` : ""}`).join("\n")}`
|
|
9062
|
+
).join("\n\n")}`;
|
|
9063
|
+
}
|
|
9064
|
+
function summarizeTaskResult(result) {
|
|
9065
|
+
const text = typeof result.result === "string" ? result.result : result.result !== void 0 ? JSON.stringify(result.result, null, 2) : "";
|
|
9066
|
+
const error2 = result.error ? `${result.error.kind}: ${result.error.message}` : void 0;
|
|
9002
9067
|
return {
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9068
|
+
taskId: result.taskId,
|
|
9069
|
+
subagentId: result.subagentId,
|
|
9070
|
+
status: result.status,
|
|
9071
|
+
summary: excerpt(text || error2 || "(no output)", 4e3),
|
|
9072
|
+
error: error2
|
|
9073
|
+
};
|
|
9074
|
+
}
|
|
9075
|
+
function assessRoleResult(role, result) {
|
|
9076
|
+
const resolvedRole = role ?? (result.subagentId.includes("review") ? "reviewer" : "verifier");
|
|
9077
|
+
const summary = summarizeTaskResult(result);
|
|
9078
|
+
const text = summary.summary;
|
|
9079
|
+
const uncertaintyFlags = extractSection(text, "Uncertainty Flags");
|
|
9080
|
+
if (result.status !== "success") {
|
|
9081
|
+
return {
|
|
9082
|
+
role: resolvedRole,
|
|
9083
|
+
subagentId: result.subagentId,
|
|
9084
|
+
taskId: result.taskId,
|
|
9085
|
+
status: result.status,
|
|
9086
|
+
verdict: "fail",
|
|
9087
|
+
summary: text,
|
|
9088
|
+
uncertaintyFlags,
|
|
9089
|
+
error: summary.error ?? result.status
|
|
9090
|
+
};
|
|
9091
|
+
}
|
|
9092
|
+
return {
|
|
9093
|
+
role: resolvedRole,
|
|
9094
|
+
subagentId: result.subagentId,
|
|
9095
|
+
taskId: result.taskId,
|
|
9096
|
+
status: result.status,
|
|
9097
|
+
verdict: parseQualityVerdict(resolvedRole, text),
|
|
9098
|
+
summary: text,
|
|
9099
|
+
uncertaintyFlags
|
|
9018
9100
|
};
|
|
9019
9101
|
}
|
|
9020
|
-
function
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
message: `Fleet shutdown complete \u2014 all subagents stopped, pending tasks drained.`
|
|
9033
|
-
};
|
|
9102
|
+
function assessQualityGate(reports) {
|
|
9103
|
+
const mustFix = [];
|
|
9104
|
+
const uncertaintyFlags = [];
|
|
9105
|
+
let hasFail = false;
|
|
9106
|
+
let hasInconclusive = false;
|
|
9107
|
+
for (const report of reports) {
|
|
9108
|
+
if (report.verdict === "fail") hasFail = true;
|
|
9109
|
+
if (report.verdict === "inconclusive") hasInconclusive = true;
|
|
9110
|
+
const blocking = extractSection(report.summary, "Must Fix") || extractSection(report.summary, "Failures") || extractSection(report.summary, "Verification Gaps");
|
|
9111
|
+
if (blocking) mustFix.push(`${report.role}: ${excerpt(blocking, 1e3)}`);
|
|
9112
|
+
if (report.uncertaintyFlags) {
|
|
9113
|
+
uncertaintyFlags.push(`${report.role}: ${excerpt(report.uncertaintyFlags, 1e3)}`);
|
|
9034
9114
|
}
|
|
9035
|
-
|
|
9115
|
+
if (report.error) mustFix.push(`${report.role}: ${report.error}`);
|
|
9116
|
+
}
|
|
9117
|
+
if (hasFail) return { verdict: "fail", passed: false, mustFix, uncertaintyFlags };
|
|
9118
|
+
if (hasInconclusive || reports.length === 0) {
|
|
9119
|
+
return { verdict: "inconclusive", passed: false, mustFix, uncertaintyFlags };
|
|
9120
|
+
}
|
|
9121
|
+
return { verdict: "pass", passed: true, mustFix, uncertaintyFlags };
|
|
9036
9122
|
}
|
|
9037
|
-
function
|
|
9038
|
-
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
inputSchema: {
|
|
9046
|
-
type: "object",
|
|
9047
|
-
properties: {
|
|
9048
|
-
action: {
|
|
9049
|
-
type: "string",
|
|
9050
|
-
enum: ["status", "usage", "health", "session"],
|
|
9051
|
-
description: "Observation to retrieve (default: status)."
|
|
9052
|
-
},
|
|
9053
|
-
subagentId: {
|
|
9054
|
-
type: "string",
|
|
9055
|
-
description: 'Subagent id (required for action: "session").'
|
|
9056
|
-
},
|
|
9057
|
-
tail: {
|
|
9058
|
-
type: "number",
|
|
9059
|
-
description: 'Number of trailing JSONL lines (action: "session" only). Omit for the full transcript.'
|
|
9060
|
-
}
|
|
9061
|
-
}
|
|
9062
|
-
},
|
|
9063
|
-
async execute(input) {
|
|
9064
|
-
const i = input ?? {};
|
|
9065
|
-
const action = i.action ?? "status";
|
|
9066
|
-
switch (action) {
|
|
9067
|
-
case "status": {
|
|
9068
|
-
const base = director.status();
|
|
9069
|
-
const fm = director.fleetManager;
|
|
9070
|
-
const stats = fm?.getFleetStats();
|
|
9071
|
-
const fleetStatus = fm?.getFleetStatus();
|
|
9072
|
-
return {
|
|
9073
|
-
action: "status",
|
|
9074
|
-
subagents: base.subagents,
|
|
9075
|
-
coordinatorStats: stats ? {
|
|
9076
|
-
total: stats.total,
|
|
9077
|
-
running: stats.running,
|
|
9078
|
-
idle: stats.idle,
|
|
9079
|
-
stopped: stats.stopped
|
|
9080
|
-
} : void 0,
|
|
9081
|
-
pending: fleetStatus?.pending ?? [],
|
|
9082
|
-
usage: fm?.snapshot()
|
|
9083
|
-
};
|
|
9084
|
-
}
|
|
9085
|
-
case "usage": {
|
|
9086
|
-
return { action: "usage", ...director.snapshot() };
|
|
9087
|
-
}
|
|
9088
|
-
case "health": {
|
|
9089
|
-
const status = director.status();
|
|
9090
|
-
const snapshot = director.snapshot();
|
|
9091
|
-
const subagents = status.subagents ?? [];
|
|
9092
|
-
const perSubagent = snapshot.perSubagent ?? {};
|
|
9093
|
-
return {
|
|
9094
|
-
action: "health",
|
|
9095
|
-
subagents: subagents.map((s) => {
|
|
9096
|
-
const usage = perSubagent[s.id];
|
|
9097
|
-
return {
|
|
9098
|
-
id: s.id,
|
|
9099
|
-
status: s.status,
|
|
9100
|
-
lastEventAt: usage?.lastEventAt,
|
|
9101
|
-
budgetPressure: {
|
|
9102
|
-
iterations: usage?.iterations,
|
|
9103
|
-
toolCalls: usage?.toolCalls,
|
|
9104
|
-
costUsd: usage?.cost
|
|
9105
|
-
}
|
|
9106
|
-
};
|
|
9107
|
-
})
|
|
9108
|
-
};
|
|
9109
|
-
}
|
|
9110
|
-
case "session": {
|
|
9111
|
-
const subagentId = i.subagentId;
|
|
9112
|
-
if (!subagentId) {
|
|
9113
|
-
return {
|
|
9114
|
-
action: "session",
|
|
9115
|
-
error: 'fleet: subagentId is required for action: "session"'
|
|
9116
|
-
};
|
|
9117
|
-
}
|
|
9118
|
-
const result = await director.readSession(subagentId, i.tail);
|
|
9119
|
-
if (!result) {
|
|
9120
|
-
return {
|
|
9121
|
-
action: "session",
|
|
9122
|
-
error: `fleet: transcript unavailable for "${subagentId}". Is sessionsRoot configured?`
|
|
9123
|
-
};
|
|
9124
|
-
}
|
|
9125
|
-
return { action: "session", ...result };
|
|
9126
|
-
}
|
|
9127
|
-
default:
|
|
9128
|
-
return {
|
|
9129
|
-
error: `fleet: unknown action "${action}". Valid: status, usage, health, session.`
|
|
9130
|
-
};
|
|
9131
|
-
}
|
|
9123
|
+
function parseQualityVerdict(role, text) {
|
|
9124
|
+
const normalized = text.toLowerCase();
|
|
9125
|
+
const verdictBlock = normalized.match(/(?:^|\n)\s*(?:#+\s*)?verdict\b[^\n]*(?:\n|:|-)?([\s\S]{0,500})/)?.[0] ?? normalized.slice(0, 1e3);
|
|
9126
|
+
if (role === "reviewer") {
|
|
9127
|
+
if (/\b(request\s+changes|needs\s+verification|reject|rejected|fail|failed|blocked)\b/.test(
|
|
9128
|
+
verdictBlock
|
|
9129
|
+
)) {
|
|
9130
|
+
return "fail";
|
|
9132
9131
|
}
|
|
9133
|
-
|
|
9132
|
+
if (/\b(approve|approved|pass|passed)\b/.test(verdictBlock)) return "pass";
|
|
9133
|
+
if (sectionHasBlockingContent(text, "Must Fix") || sectionHasBlockingContent(text, "Verification Gaps")) {
|
|
9134
|
+
return "fail";
|
|
9135
|
+
}
|
|
9136
|
+
return "inconclusive";
|
|
9137
|
+
}
|
|
9138
|
+
if (/\b(fail|failed|blocked|red)\b/.test(verdictBlock)) return "fail";
|
|
9139
|
+
if (/\b(pass|passed|green|approve|approved)\b/.test(verdictBlock)) return "pass";
|
|
9140
|
+
if (sectionHasBlockingContent(text, "Failures")) return "fail";
|
|
9141
|
+
return "inconclusive";
|
|
9142
|
+
}
|
|
9143
|
+
function sectionHasBlockingContent(text, heading) {
|
|
9144
|
+
const section = extractSection(text, heading);
|
|
9145
|
+
if (!section) return false;
|
|
9146
|
+
return !/^\s*(none|n\/a|no\b|no issues|empty|\(none\))\s*\.?\s*$/i.test(section.trim());
|
|
9147
|
+
}
|
|
9148
|
+
function extractSection(text, heading) {
|
|
9149
|
+
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9150
|
+
const pattern = new RegExp(
|
|
9151
|
+
`(?:^|\\n)\\s*#{1,6}\\s*${escaped}\\s*\\n([\\s\\S]*?)(?=\\n\\s*#{1,6}\\s+|$)`,
|
|
9152
|
+
"i"
|
|
9153
|
+
);
|
|
9154
|
+
const match = text.match(pattern);
|
|
9155
|
+
const body = match?.[1]?.trim();
|
|
9156
|
+
return body ? body : void 0;
|
|
9157
|
+
}
|
|
9158
|
+
function excerpt(text, max) {
|
|
9159
|
+
if (text.length <= max) return text;
|
|
9160
|
+
return `${text.slice(0, max - 20).trimEnd()}
|
|
9161
|
+
...(truncated)`;
|
|
9134
9162
|
}
|
|
9135
9163
|
|
|
9136
9164
|
// src/coordination/director-tools.ts
|
|
@@ -9165,13 +9193,13 @@ function makeSpawnTool(director, roster) {
|
|
|
9165
9193
|
const inputSchema = {
|
|
9166
9194
|
type: "object",
|
|
9167
9195
|
properties: {
|
|
9168
|
-
|
|
9196
|
+
description: {
|
|
9169
9197
|
type: "string",
|
|
9170
|
-
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."
|
|
9171
9199
|
},
|
|
9172
|
-
|
|
9200
|
+
role: {
|
|
9173
9201
|
type: "string",
|
|
9174
|
-
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."
|
|
9175
9203
|
},
|
|
9176
9204
|
name: {
|
|
9177
9205
|
type: "string",
|
|
@@ -9217,7 +9245,7 @@ function makeSpawnTool(director, roster) {
|
|
|
9217
9245
|
return {
|
|
9218
9246
|
name: "spawn_subagent",
|
|
9219
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.",
|
|
9220
|
-
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.",
|
|
9221
9249
|
permission: "auto",
|
|
9222
9250
|
mutating: false,
|
|
9223
9251
|
capabilities: [ToolCapabilities.SUBAGENT_SPAWN],
|
|
@@ -9721,8 +9749,9 @@ function composeDirectorPrompt(parts = {}) {
|
|
|
9721
9749
|
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
9722
9750
|
if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
|
|
9723
9751
|
if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
|
|
9724
|
-
sections.push(
|
|
9725
|
-
|
|
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
|
+
);
|
|
9726
9755
|
}
|
|
9727
9756
|
if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
|
|
9728
9757
|
sections.push(parts.basePrompt.trim());
|
|
@@ -9759,13 +9788,23 @@ ${parts.skills.trim()}`);
|
|
|
9759
9788
|
}
|
|
9760
9789
|
return sections.join("\n\n");
|
|
9761
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
|
+
}
|
|
9762
9796
|
function rosterSummaryFromConfigs(roster) {
|
|
9763
9797
|
const lines = [];
|
|
9764
9798
|
for (const [roleId, cfg] of Object.entries(roster)) {
|
|
9765
9799
|
const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
|
|
9766
|
-
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;
|
|
9767
9806
|
const tail = headline ? ` \u2014 ${headline}` : "";
|
|
9768
|
-
lines.push(`- ${roleId}
|
|
9807
|
+
lines.push(`- ${roleId}${label}${tag}${tail}`);
|
|
9769
9808
|
}
|
|
9770
9809
|
return lines.join("\n");
|
|
9771
9810
|
}
|
|
@@ -11367,6 +11406,12 @@ function getModelFamilyRatio(calibrationKey) {
|
|
|
11367
11406
|
}
|
|
11368
11407
|
|
|
11369
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
|
+
}
|
|
11370
11415
|
var ConversationState = class {
|
|
11371
11416
|
ctx;
|
|
11372
11417
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -11434,15 +11479,32 @@ var ConversationState = class {
|
|
|
11434
11479
|
*/
|
|
11435
11480
|
overflowCount(arr) {
|
|
11436
11481
|
let drop = Context.MAX_MESSAGES > 0 ? Math.max(0, arr.length - Context.MAX_MESSAGES) : 0;
|
|
11437
|
-
if (Context.MAX_MESSAGE_TOKENS <= 0) return drop;
|
|
11482
|
+
if (Context.MAX_MESSAGE_TOKENS <= 0) return this.protocolSafeDropCount(arr, drop);
|
|
11438
11483
|
let total = 0;
|
|
11439
11484
|
for (let i = drop; i < arr.length; i++) total += arr[i]?._estTokens ?? 0;
|
|
11440
|
-
if (total <= Context.MAX_MESSAGE_TOKENS) return drop;
|
|
11485
|
+
if (total <= Context.MAX_MESSAGE_TOKENS) return this.protocolSafeDropCount(arr, drop);
|
|
11441
11486
|
while (drop < arr.length - 1 && total > Context.MAX_MESSAGE_TOKENS) {
|
|
11442
11487
|
total -= arr[drop]?._estTokens ?? 0;
|
|
11443
11488
|
drop++;
|
|
11444
11489
|
}
|
|
11445
|
-
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;
|
|
11446
11508
|
}
|
|
11447
11509
|
/**
|
|
11448
11510
|
* Append a content block to the trailing user message's content array.
|
|
@@ -11847,6 +11909,28 @@ var Context = class _Context {
|
|
|
11847
11909
|
return _Context.CONVERSATION_JOURNAL_MAX_BYTES + 1;
|
|
11848
11910
|
}
|
|
11849
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
|
+
}
|
|
11850
11934
|
enqueueConversationJournal(event, writer) {
|
|
11851
11935
|
const bytes = this.conversationJournalBytes(event);
|
|
11852
11936
|
const shouldSnapshot = event.type === "messages_replaced" || this._conversationJournalQueue.length >= _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes + bytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES;
|
|
@@ -11864,18 +11948,21 @@ var Context = class _Context {
|
|
|
11864
11948
|
this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - queued.bytes);
|
|
11865
11949
|
this._conversationJournalQueue.splice(index, 1);
|
|
11866
11950
|
}
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
this._conversationJournalBytes += snapshotBytes;
|
|
11870
|
-
}
|
|
11951
|
+
this._conversationJournalQueue.push({ event: snapshot, bytes: snapshotBytes, writer });
|
|
11952
|
+
this._conversationJournalBytes += snapshotBytes;
|
|
11871
11953
|
} else {
|
|
11872
11954
|
this._conversationJournalQueue.push({ event, bytes, writer });
|
|
11873
11955
|
this._conversationJournalBytes += bytes;
|
|
11874
11956
|
}
|
|
11875
11957
|
while (this._conversationJournalQueue.length > _Context.CONVERSATION_JOURNAL_MAX_EVENTS || this._conversationJournalBytes > _Context.CONVERSATION_JOURNAL_MAX_BYTES) {
|
|
11876
|
-
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);
|
|
11877
11963
|
if (!dropped) break;
|
|
11878
11964
|
this._conversationJournalBytes = Math.max(0, this._conversationJournalBytes - dropped.bytes);
|
|
11965
|
+
this.warnConversationJournalDrop(dropped.event.type);
|
|
11879
11966
|
}
|
|
11880
11967
|
this.startConversationJournalDrain();
|
|
11881
11968
|
}
|
|
@@ -13064,6 +13151,20 @@ function sessionScopedPath(dir, sessionId, suffix) {
|
|
|
13064
13151
|
}
|
|
13065
13152
|
return resolved;
|
|
13066
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
|
+
}
|
|
13067
13168
|
function invalid(sessionId) {
|
|
13068
13169
|
return new FsError({
|
|
13069
13170
|
message: `Invalid sessionId: ${sessionId}`,
|
|
@@ -15559,6 +15660,15 @@ async function validateResumeFileObservations(events, projectRoot) {
|
|
|
15559
15660
|
staleFiles: results.filter((entry) => entry !== null)
|
|
15560
15661
|
};
|
|
15561
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
|
+
}
|
|
15562
15672
|
function formatResumeValidationNotice(validation, projectRoot) {
|
|
15563
15673
|
if (validation.staleFiles.length === 0) return null;
|
|
15564
15674
|
const root = path18.resolve(projectRoot);
|
|
@@ -15669,9 +15779,6 @@ async function assertSessionCanBeDeleted(sessionId, isSessionInUse) {
|
|
|
15669
15779
|
function shouldSkipSessionDirectoryEntry(name) {
|
|
15670
15780
|
return name.startsWith(".") && name !== ".wrongstack" || name === "shared" || name === "subagents" || name === "attachments";
|
|
15671
15781
|
}
|
|
15672
|
-
function isSessionJsonlFileName(name) {
|
|
15673
|
-
return name.endsWith(".jsonl") && name !== "_index.jsonl";
|
|
15674
|
-
}
|
|
15675
15782
|
|
|
15676
15783
|
// src/storage/session-store/directory-session-files.ts
|
|
15677
15784
|
import * as fsp11 from "node:fs/promises";
|
|
@@ -15696,7 +15803,7 @@ async function collectSessionFiles(dir, prefix = "", depth = 0) {
|
|
|
15696
15803
|
if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
|
|
15697
15804
|
if (entry.isDirectory()) {
|
|
15698
15805
|
dirEntries.push(entry);
|
|
15699
|
-
} else if (entry.isFile() &&
|
|
15806
|
+
} else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
|
|
15700
15807
|
files.push({ id: sessionIdForFile(prefix, entry.name), filePath: path21.join(dir, entry.name) });
|
|
15701
15808
|
}
|
|
15702
15809
|
}
|
|
@@ -15720,7 +15827,7 @@ async function collectSessionIds(dir, prefix = "", depth = 0) {
|
|
|
15720
15827
|
if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
|
|
15721
15828
|
if (entry.isDirectory()) {
|
|
15722
15829
|
dirEntries.push(entry);
|
|
15723
|
-
} else if (entry.isFile() &&
|
|
15830
|
+
} else if (entry.isFile() && isSessionTranscriptFileName(entry.name)) {
|
|
15724
15831
|
fileIds.push(sessionIdForFile(prefix, entry.name));
|
|
15725
15832
|
}
|
|
15726
15833
|
}
|
|
@@ -15822,13 +15929,13 @@ function inheritsIntoFork(event) {
|
|
|
15822
15929
|
|
|
15823
15930
|
// src/storage/session-store/fork-session.ts
|
|
15824
15931
|
async function forkSession(host, id, opts = {}) {
|
|
15825
|
-
const
|
|
15826
|
-
let boundary =
|
|
15932
|
+
const parentEvents = await host.readRawEvents(id);
|
|
15933
|
+
let boundary = parentEvents.length - 1;
|
|
15827
15934
|
let targetCheckpoint;
|
|
15828
15935
|
if (opts.checkpointPromptIndex !== void 0) {
|
|
15829
15936
|
boundary = -1;
|
|
15830
|
-
for (let i = 0; i <
|
|
15831
|
-
const event =
|
|
15937
|
+
for (let i = 0; i < parentEvents.length; i++) {
|
|
15938
|
+
const event = parentEvents[i];
|
|
15832
15939
|
if (event?.type === "checkpoint" && event.promptIndex === opts.checkpointPromptIndex) {
|
|
15833
15940
|
boundary = i;
|
|
15834
15941
|
targetCheckpoint = event;
|
|
@@ -15838,15 +15945,18 @@ async function forkSession(host, id, opts = {}) {
|
|
|
15838
15945
|
throw new Error(`Checkpoint ${opts.checkpointPromptIndex} not found in session "${id}"`);
|
|
15839
15946
|
}
|
|
15840
15947
|
}
|
|
15841
|
-
const parentPrefix =
|
|
15948
|
+
const parentPrefix = parentEvents.slice(0, boundary + 1);
|
|
15842
15949
|
const workspaceCheckpoint = targetCheckpoint?.workspaceCheckpoint;
|
|
15843
15950
|
const checkpointHash = createHash5("sha256").update(parentPrefix.map((event) => JSON.stringify(event)).join("\n") + "\n", "utf8").digest("hex");
|
|
15844
15951
|
const inherited = parentPrefix.filter(inheritsIntoFork);
|
|
15952
|
+
const start = parentEvents.find(
|
|
15953
|
+
(event) => event.type === "session_start"
|
|
15954
|
+
);
|
|
15845
15955
|
const writer = await host.create({
|
|
15846
15956
|
id: "",
|
|
15847
|
-
title:
|
|
15848
|
-
model:
|
|
15849
|
-
provider:
|
|
15957
|
+
title: "",
|
|
15958
|
+
model: start?.model,
|
|
15959
|
+
provider: start?.provider
|
|
15850
15960
|
});
|
|
15851
15961
|
try {
|
|
15852
15962
|
await writer.append({
|
|
@@ -15952,6 +16062,17 @@ var SessionLoadCache = class {
|
|
|
15952
16062
|
this.entries.clear();
|
|
15953
16063
|
this.bytes = 0;
|
|
15954
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
|
+
*/
|
|
15955
16076
|
getFresh(id, stat15, full) {
|
|
15956
16077
|
const cached = this.entries.get(id);
|
|
15957
16078
|
if (!cached || cached.mtimeMs !== stat15.mtimeMs || cached.size !== stat15.size) {
|
|
@@ -15959,8 +16080,11 @@ var SessionLoadCache = class {
|
|
|
15959
16080
|
}
|
|
15960
16081
|
this.entries.delete(id);
|
|
15961
16082
|
this.entries.set(id, cached);
|
|
15962
|
-
|
|
15963
|
-
|
|
16083
|
+
return {
|
|
16084
|
+
...cached.data,
|
|
16085
|
+
messages: full ? [...cached.data.messages] : [],
|
|
16086
|
+
events: [...cached.data.events]
|
|
16087
|
+
};
|
|
15964
16088
|
}
|
|
15965
16089
|
set(id, stat15, data) {
|
|
15966
16090
|
this.delete(id);
|
|
@@ -16149,6 +16273,9 @@ function stripSnapshotPayload(event) {
|
|
|
16149
16273
|
event.messagesOmitted = event.messages.length;
|
|
16150
16274
|
event.messages = [];
|
|
16151
16275
|
}
|
|
16276
|
+
function isStrippedSnapshot(event) {
|
|
16277
|
+
return event.messages.length === 0 && typeof event.messagesOmitted === "number" && event.messagesOmitted > 0;
|
|
16278
|
+
}
|
|
16152
16279
|
function isSessionEventLike(value) {
|
|
16153
16280
|
return value !== null && typeof value === "object" && typeof value.type === "string" && typeof value.ts === "string";
|
|
16154
16281
|
}
|
|
@@ -16178,7 +16305,12 @@ function replaySessionEvent(params) {
|
|
|
16178
16305
|
emitDamaged(params, `Ignored malformed message_updated event at index ${ev.index}`);
|
|
16179
16306
|
}
|
|
16180
16307
|
} else if (ev.type === "messages_replaced" && ev.version === 1) {
|
|
16181
|
-
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)) {
|
|
16182
16314
|
exactJournalActive = true;
|
|
16183
16315
|
} else {
|
|
16184
16316
|
emitDamaged(params, "Ignored malformed messages_replaced event");
|
|
@@ -16194,9 +16326,16 @@ function replaySessionEvent(params) {
|
|
|
16194
16326
|
emitDamaged(params, `Ignored malformed messages_dropped event (count ${String(ev.count)})`);
|
|
16195
16327
|
}
|
|
16196
16328
|
} else if (ev.type === "context_snapshot") {
|
|
16197
|
-
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)) {
|
|
16198
16335
|
emitDamaged(params, "Ignored malformed context_snapshot event");
|
|
16199
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`);
|
|
16200
16339
|
} else if (!exactJournalActive && ev.type === "user_input") {
|
|
16201
16340
|
openToolUses.clear();
|
|
16202
16341
|
messages.push({ role: "user", content: ev.content, ts: ev.ts });
|
|
@@ -16249,7 +16388,7 @@ function emitDamaged(params, detail) {
|
|
|
16249
16388
|
import * as fsp13 from "node:fs/promises";
|
|
16250
16389
|
import * as path22 from "node:path";
|
|
16251
16390
|
function isPrunableSessionJsonl(name) {
|
|
16252
|
-
return
|
|
16391
|
+
return isSessionTranscriptFileName(name);
|
|
16253
16392
|
}
|
|
16254
16393
|
async function pruneSessionFiles(storeDir, maxAgeDays, deleteSession) {
|
|
16255
16394
|
const cutoff = Date.now() - maxAgeDays * 864e5;
|
|
@@ -16772,6 +16911,21 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
16772
16911
|
async fork(id, opts = {}) {
|
|
16773
16912
|
return forkSession(this, id, opts);
|
|
16774
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
|
+
}
|
|
16775
16929
|
/**
|
|
16776
16930
|
* Capture the deterministic post-tool workspace identity through the store-owned CAS.
|
|
16777
16931
|
*/
|
|
@@ -16860,14 +17014,15 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
16860
17014
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
16861
17015
|
});
|
|
16862
17016
|
}
|
|
17017
|
+
const carriedMessages = data.messages.filter((message) => !isResumeNoticeMessage(message));
|
|
16863
17018
|
const resumedData = {
|
|
16864
17019
|
...data,
|
|
16865
17020
|
...resumeValidation ? { resumeValidation } : {},
|
|
16866
|
-
|
|
17021
|
+
messages: [...carriedMessages, ...noticeMessages]
|
|
16867
17022
|
};
|
|
16868
17023
|
let handle;
|
|
16869
17024
|
try {
|
|
16870
|
-
handle = await
|
|
17025
|
+
handle = await openSessionForAppend(file);
|
|
16871
17026
|
} catch (err) {
|
|
16872
17027
|
emitSessionStoreError(this.events, canonicalId, file, "resume", toErrorMessage(err), false);
|
|
16873
17028
|
throw new Error(
|
|
@@ -17046,10 +17201,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
17046
17201
|
const limit = criteria.limit ?? 100;
|
|
17047
17202
|
if (this.catalogClient) {
|
|
17048
17203
|
const records = await this.catalogClient.call("list_catalog", {
|
|
17049
|
-
limit
|
|
17050
|
-
...criteria
|
|
17204
|
+
limit,
|
|
17205
|
+
...criteria
|
|
17051
17206
|
});
|
|
17052
|
-
return this.scrubSummaries(records)
|
|
17207
|
+
return this.scrubSummaries(records);
|
|
17053
17208
|
}
|
|
17054
17209
|
try {
|
|
17055
17210
|
const indexed = await this.readIndex();
|
|
@@ -17274,11 +17429,11 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
17274
17429
|
return shardKeys;
|
|
17275
17430
|
}
|
|
17276
17431
|
async readOrBuildShardManifest(shardKey) {
|
|
17277
|
-
const cached = this.shardManifestCache.get(shardKey);
|
|
17278
|
-
if (cached) return cached;
|
|
17279
17432
|
const manifestPath = this.shardManifestPath(shardKey);
|
|
17433
|
+
const cached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
|
|
17434
|
+
if (cached) return cached;
|
|
17280
17435
|
return withFileLock(manifestPath, async () => {
|
|
17281
|
-
const lockedCached = this.
|
|
17436
|
+
const lockedCached = await this.freshShardManifestCacheEntry(shardKey, manifestPath);
|
|
17282
17437
|
if (lockedCached) return lockedCached;
|
|
17283
17438
|
const entry = await readOrBuildShardManifestEntry({
|
|
17284
17439
|
shardKey,
|
|
@@ -17289,10 +17444,38 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
17289
17444
|
summaryHeaderFor: (ref) => this.summaryHeaderFor(ref),
|
|
17290
17445
|
summaryFor: (id) => this.summaryFor(id)
|
|
17291
17446
|
});
|
|
17292
|
-
|
|
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
|
+
}
|
|
17293
17458
|
return entry;
|
|
17294
17459
|
});
|
|
17295
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
|
+
}
|
|
17296
17479
|
async collectSessionFilesInShard(shardKey) {
|
|
17297
17480
|
const dir = shardKey ? path23.join(this.dir, shardKey) : this.dir;
|
|
17298
17481
|
const entries = await this.collectSessionFiles(dir, shardKey);
|
|
@@ -17572,6 +17755,23 @@ var DefaultSessionStore = class _DefaultSessionStore {
|
|
|
17572
17755
|
});
|
|
17573
17756
|
}
|
|
17574
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
|
+
}
|
|
17575
17775
|
|
|
17576
17776
|
// src/coordination/director-session.ts
|
|
17577
17777
|
function makeDirectorSessionFactory(opts) {
|
|
@@ -22148,6 +22348,9 @@ var DEFAULT_TOOLS_CONFIG = Object.freeze({
|
|
|
22148
22348
|
disabledTools: Object.freeze([]),
|
|
22149
22349
|
autoExtendLimit: true,
|
|
22150
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,
|
|
22151
22354
|
loopDetection: Object.freeze({
|
|
22152
22355
|
mode: "steer-then-cut",
|
|
22153
22356
|
steerThreshold: 3,
|
|
@@ -28051,7 +28254,7 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
28051
28254
|
} : { decision: "allow" };
|
|
28052
28255
|
}
|
|
28053
28256
|
const task = identity.taskId ? board.tasks.find((candidate) => candidate.id === identity.taskId) : void 0;
|
|
28054
|
-
if (governanceRequired) {
|
|
28257
|
+
if (governanceRequired && board.lifecycle?.mode === "managed") {
|
|
28055
28258
|
if (!identity.taskId) {
|
|
28056
28259
|
return {
|
|
28057
28260
|
decision: "block",
|
|
@@ -28077,9 +28280,11 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
28077
28280
|
};
|
|
28078
28281
|
}
|
|
28079
28282
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
28283
|
+
const lifecycleStage = task.lifecycle?.currentStage ?? "missing";
|
|
28284
|
+
const assignmentStatus = task.assignment?.status ?? "missing";
|
|
28080
28285
|
return {
|
|
28081
28286
|
decision: "block",
|
|
28082
|
-
reason:
|
|
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.`,
|
|
28083
28288
|
boardId: board.id,
|
|
28084
28289
|
taskId: task.id
|
|
28085
28290
|
};
|
|
@@ -32974,23 +33179,21 @@ function unwrapDataKey(buf, keyFile) {
|
|
|
32974
33179
|
});
|
|
32975
33180
|
}
|
|
32976
33181
|
}
|
|
32977
|
-
function
|
|
32978
|
-
if (process.platform === "win32") return;
|
|
33182
|
+
function keyFileNeedsHardening(keyFile, opts) {
|
|
33183
|
+
if (process.platform === "win32") return false;
|
|
32979
33184
|
const warn = opts?.warn ?? ((msg) => console.warn(msg));
|
|
32980
33185
|
try {
|
|
32981
33186
|
const stat15 = fs14.statSync(keyFile);
|
|
32982
33187
|
const actualMode = stat15.mode & 511;
|
|
32983
33188
|
if (actualMode !== KEY_FILE_MODE) {
|
|
32984
|
-
void restrictFilePermissions(keyFile, {
|
|
32985
|
-
label: "secret-vault",
|
|
32986
|
-
warn
|
|
32987
|
-
}).catch(() => void 0);
|
|
32988
33189
|
warn(
|
|
32989
33190
|
`Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Hardening\u2026`
|
|
32990
33191
|
);
|
|
33192
|
+
return true;
|
|
32991
33193
|
}
|
|
32992
33194
|
} catch {
|
|
32993
33195
|
}
|
|
33196
|
+
return false;
|
|
32994
33197
|
}
|
|
32995
33198
|
function writeKeyFileAtomicSync(keyFile, content) {
|
|
32996
33199
|
const tmp = `${keyFile}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
@@ -33056,6 +33259,14 @@ var DefaultSecretVault = class {
|
|
|
33056
33259
|
}).catch(() => void 0);
|
|
33057
33260
|
this.pendingHardening.push(p);
|
|
33058
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
|
+
}
|
|
33059
33270
|
/** Flush all pending key-file hardening promises. */
|
|
33060
33271
|
flushHardening() {
|
|
33061
33272
|
if (this.pendingHardening.length === 0) return Promise.resolve();
|
|
@@ -33143,7 +33354,7 @@ var DefaultSecretVault = class {
|
|
|
33143
33354
|
writeKeyFileAtomicSync(this.keyFile, keyFileBuf);
|
|
33144
33355
|
}
|
|
33145
33356
|
this.scheduleKeyHardening();
|
|
33146
|
-
|
|
33357
|
+
this.checkKeyFilePermissions();
|
|
33147
33358
|
this.key = newKey;
|
|
33148
33359
|
this._keyVersion = newVersion;
|
|
33149
33360
|
return { oldVersion, newVersion };
|
|
@@ -33159,12 +33370,9 @@ var DefaultSecretVault = class {
|
|
|
33159
33370
|
const passphrase = getVaultPassphrase();
|
|
33160
33371
|
if (!passphrase || !this.key) return;
|
|
33161
33372
|
try {
|
|
33162
|
-
writeKeyFileAtomicSync(
|
|
33163
|
-
this.keyFile,
|
|
33164
|
-
wrapDataKey(this.key, this._keyVersion, passphrase)
|
|
33165
|
-
);
|
|
33373
|
+
writeKeyFileAtomicSync(this.keyFile, wrapDataKey(this.key, this._keyVersion, passphrase));
|
|
33166
33374
|
this.scheduleKeyHardening();
|
|
33167
|
-
|
|
33375
|
+
this.checkKeyFilePermissions();
|
|
33168
33376
|
} catch {
|
|
33169
33377
|
}
|
|
33170
33378
|
}
|
|
@@ -33176,13 +33384,13 @@ var DefaultSecretVault = class {
|
|
|
33176
33384
|
const { key: key2, version } = unwrapDataKey(buf, this.keyFile);
|
|
33177
33385
|
this.key = key2;
|
|
33178
33386
|
this._keyVersion = version;
|
|
33179
|
-
|
|
33387
|
+
this.checkKeyFilePermissions();
|
|
33180
33388
|
return this.key;
|
|
33181
33389
|
}
|
|
33182
33390
|
if (buf.length === KEY_BYTES) {
|
|
33183
33391
|
this.key = buf;
|
|
33184
33392
|
this._keyVersion = 1;
|
|
33185
|
-
|
|
33393
|
+
this.checkKeyFilePermissions();
|
|
33186
33394
|
this.migrateToWrappedIfPassphrase();
|
|
33187
33395
|
return this.key;
|
|
33188
33396
|
}
|
|
@@ -33206,7 +33414,7 @@ var DefaultSecretVault = class {
|
|
|
33206
33414
|
}
|
|
33207
33415
|
this.key = Buffer.from(key2);
|
|
33208
33416
|
this._keyVersion = version;
|
|
33209
|
-
|
|
33417
|
+
this.checkKeyFilePermissions();
|
|
33210
33418
|
this.migrateToWrappedIfPassphrase();
|
|
33211
33419
|
return this.key;
|
|
33212
33420
|
}
|
|
@@ -33233,13 +33441,13 @@ var DefaultSecretVault = class {
|
|
|
33233
33441
|
const { key: winnerKey, version } = unwrapDataKey(buf, this.keyFile);
|
|
33234
33442
|
this.key = winnerKey;
|
|
33235
33443
|
this._keyVersion = version;
|
|
33236
|
-
|
|
33444
|
+
this.checkKeyFilePermissions();
|
|
33237
33445
|
return this.key;
|
|
33238
33446
|
}
|
|
33239
33447
|
if (buf.length === KEY_BYTES) {
|
|
33240
33448
|
this.key = buf;
|
|
33241
33449
|
this._keyVersion = 1;
|
|
33242
|
-
|
|
33450
|
+
this.checkKeyFilePermissions();
|
|
33243
33451
|
return this.key;
|
|
33244
33452
|
}
|
|
33245
33453
|
if (buf.length === VERSIONED_KEY_FILE_SIZE) {
|
|
@@ -33255,7 +33463,7 @@ var DefaultSecretVault = class {
|
|
|
33255
33463
|
const winnerKey = buf.subarray(KEY_FILE_MAGIC.length + 1);
|
|
33256
33464
|
this.key = Buffer.from(winnerKey);
|
|
33257
33465
|
this._keyVersion = version;
|
|
33258
|
-
|
|
33466
|
+
this.checkKeyFilePermissions();
|
|
33259
33467
|
return this.key;
|
|
33260
33468
|
}
|
|
33261
33469
|
throw new ConfigError({
|
|
@@ -33506,6 +33714,7 @@ var CONFIG_BEHAVIOR_DEFAULTS = {
|
|
|
33506
33714
|
disabledTools: DEFAULT_TOOLS_CONFIG.disabledTools,
|
|
33507
33715
|
autoExtendLimit: DEFAULT_TOOLS_CONFIG.autoExtendLimit,
|
|
33508
33716
|
restrictToProjectRoot: DEFAULT_TOOLS_CONFIG.restrictToProjectRoot,
|
|
33717
|
+
kanbanGovernance: DEFAULT_TOOLS_CONFIG.kanbanGovernance,
|
|
33509
33718
|
loopDetection: DEFAULT_TOOLS_CONFIG.loopDetection
|
|
33510
33719
|
},
|
|
33511
33720
|
log: { level: "info" },
|
|
@@ -33890,6 +34099,14 @@ var IN_PROJECT_DENIED_PATHS = [
|
|
|
33890
34099
|
{
|
|
33891
34100
|
path: "tools.restrictToProjectRoot",
|
|
33892
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."
|
|
33893
34110
|
}
|
|
33894
34111
|
];
|
|
33895
34112
|
function deleteNestedPath(target, path45) {
|