@rallycry/conveyor-agent 11.0.22 → 11.0.24
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/{boot-Z3EBJ7GX.js → boot-65PTSADA.js} +30 -2
- package/dist/{chunk-EIHZRSYI.js → chunk-EITUTXXJ.js} +1 -1
- package/dist/{chunk-RW6V7KCE.js → chunk-HW6SHIL7.js} +698 -28
- package/dist/{chunk-WNSX5ZEQ.js → chunk-IWWBG5FI.js} +1 -1
- package/dist/{chunk-W5INK3NE.js → chunk-WCJ6HRVU.js} +3 -0
- package/dist/{chunk-OMD77OIJ.js → chunk-YNI2HFZE.js} +281 -95
- package/dist/cli.js +7 -7
- package/dist/index.d.ts +22 -2
- package/dist/index.js +3 -3
- package/dist/{serve-boot-4O5NJ77J.js → serve-boot-THPACDVI.js} +2 -2
- package/dist/{server-L5CQ5EJB.js → server-E6IYHSMN.js} +2 -2
- package/package.json +1 -1
- package/skills/conveyor-build/SKILL.md +21 -0
- package/skills/conveyor-consensus/SKILL.md +44 -1
- package/skills/conveyor-plan/SKILL.md +23 -0
- package/skills/conveyor-plan/references/product-decisions.md +103 -0
- package/skills/conveyor-review/SKILL.md +11 -0
- package/skills/conveyor-triage/SKILL.md +8 -0
- package/skills/conveyor-workflows/SKILL.md +16 -3
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
spawnOptionsFingerprint,
|
|
30
30
|
transcriptSize,
|
|
31
31
|
turnOptionsFrom
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-WCJ6HRVU.js";
|
|
33
33
|
import {
|
|
34
34
|
AGENT_STATUS_REASON_USER_QUESTION,
|
|
35
35
|
AgentConnection,
|
|
@@ -91,7 +91,7 @@ import {
|
|
|
91
91
|
statWorkspacePath,
|
|
92
92
|
updateRemoteToken,
|
|
93
93
|
verifyGitCredential
|
|
94
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-YNI2HFZE.js";
|
|
95
95
|
import {
|
|
96
96
|
registerBootMilestoneSocketFallback,
|
|
97
97
|
reportBootMilestone
|
|
@@ -831,9 +831,11 @@ function mapUsage(message) {
|
|
|
831
831
|
if (!isRecord(usage)) return void 0;
|
|
832
832
|
const result = {};
|
|
833
833
|
const input = numberField(usage, "input_tokens");
|
|
834
|
+
const output = numberField(usage, "output_tokens");
|
|
834
835
|
const cacheRead = numberField(usage, "cache_read_input_tokens");
|
|
835
836
|
const cacheCreation = numberField(usage, "cache_creation_input_tokens");
|
|
836
837
|
if (input !== void 0) result.input_tokens = input;
|
|
838
|
+
if (output !== void 0) result.output_tokens = output;
|
|
837
839
|
if (cacheRead !== void 0) result.cache_read_input_tokens = cacheRead;
|
|
838
840
|
if (cacheCreation !== void 0) result.cache_creation_input_tokens = cacheCreation;
|
|
839
841
|
return result;
|
|
@@ -1807,8 +1809,13 @@ async function writeHookSettings(dir, opts = {}) {
|
|
|
1807
1809
|
// `--dangerously-skip-permissions`.
|
|
1808
1810
|
// NOTE: a bare "*" rule is INVALID (the CLI rejects it and parks the TUI on
|
|
1809
1811
|
// a Settings Warning dialog at boot) — hence the explicit ALLOW_RULES list.
|
|
1812
|
+
// `deny` carries the tools no pod agent may use (Artifact publishes to an
|
|
1813
|
+
// off-platform claude.ai URL; deliverables belong on the card). Bare tool
|
|
1814
|
+
// names are the valid rule form. The key is omitted when empty — see
|
|
1815
|
+
// HookSettingsOptions.disallowedTools for why that matters.
|
|
1810
1816
|
permissions: {
|
|
1811
|
-
allow: ALLOW_RULES
|
|
1817
|
+
allow: ALLOW_RULES,
|
|
1818
|
+
...opts.disallowedTools && opts.disallowedTools.length > 0 ? { deny: opts.disallowedTools } : {}
|
|
1812
1819
|
},
|
|
1813
1820
|
hooks: {
|
|
1814
1821
|
PreToolUse: buildPreToolUseHooks(helperPath, opts.gateAllTools === true),
|
|
@@ -2606,6 +2613,7 @@ var ClaudeTuiAdapter = class {
|
|
|
2606
2613
|
permissionMode: options.permissionMode,
|
|
2607
2614
|
settingsPath: input.settingsPath ?? "",
|
|
2608
2615
|
...options.appendSystemPrompt ? { appendSystemPrompt: options.appendSystemPrompt } : {},
|
|
2616
|
+
...options.disallowedTools?.length ? { disallowedTools: options.disallowedTools } : {},
|
|
2609
2617
|
...input.mcpConfigPath ? { mcpConfigPath: input.mcpConfigPath, strictMcpConfig: true } : {}
|
|
2610
2618
|
});
|
|
2611
2619
|
return {
|
|
@@ -3155,7 +3163,8 @@ var PtySession = class {
|
|
|
3155
3163
|
);
|
|
3156
3164
|
await this.socket.listen();
|
|
3157
3165
|
const { settingsPath } = await writeHookSettings(this.tempDir, {
|
|
3158
|
-
gateAllTools: this.options.permissionMode === "plan"
|
|
3166
|
+
gateAllTools: this.options.permissionMode === "plan",
|
|
3167
|
+
...this.options.disallowedTools?.length ? { disallowedTools: this.options.disallowedTools } : {}
|
|
3159
3168
|
});
|
|
3160
3169
|
await this.setupToolServers();
|
|
3161
3170
|
const transcriptPath = sessionTranscriptPath(this.options.cwd, sessionId);
|
|
@@ -5641,7 +5650,8 @@ function buildDiscoveryPrompt(context, runnerMode) {
|
|
|
5641
5650
|
] : [],
|
|
5642
5651
|
``,
|
|
5643
5652
|
`### Contracts this environment enforces`,
|
|
5644
|
-
`- You are READ-ONLY. File writes are denied outside \`.claude/plans/\`, and so are the commands that would start the work: git commit/push/merge/rebase/checkout, git apply, gh pr create, package publish. Everything else runs without an approval prompt \u2014 the pod is a sandbox, so investigate freely (build, test, query the dev DB, read logs).`,
|
|
5653
|
+
`- You are READ-ONLY. File writes are denied outside \`.claude/plans/\` and \`/tmp/\`, and so are the commands that would start the work: git commit/push/merge/rebase/checkout, git apply, gh pr create, package publish. Everything else runs without an approval prompt \u2014 the pod is a sandbox, so investigate freely (build, test, query the dev DB, read logs).`,
|
|
5654
|
+
`- Research deliverables the team should keep (a diagram, an HTML report, a screenshot, a data export) go on the card with \`mcp__conveyor__upload_attachment\`: write the file under \`/tmp/\` first, and pass \`title\` as the caption so it shows in the thread. Never publish one as a Claude artifact.`,
|
|
5645
5655
|
`- A plan file on disk is NOT the plan. Only \`update_task\` saves it to the card.`,
|
|
5646
5656
|
`- **ExitPlanMode is the LAST step and it validates.** It fails until the card has a plan, story points, risk, and a title \u2014 the plan comes from \`update_task\`, and story points, risk and title from \`update_task_properties\`. Calling it early wastes a turn on a denial.`,
|
|
5647
5657
|
`- ExitPlanMode does NOT start building. It parks this session for human review; the team decides when to build.`,
|
|
@@ -6340,7 +6350,7 @@ Working rules:`,
|
|
|
6340
6350
|
`- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output.`,
|
|
6341
6351
|
`- App servers (including development and production servers) keep running after startup. Start them with the harness background/session facility, then run a separate bounded HTTP readiness check and continue verification. Never wait for server exit or a completion notification to mean the app is ready. If readiness times out, inspect build output and process/resource state before restarting.`,
|
|
6342
6352
|
runtimeTui === "codex" ? `- Waiting on long-running commands: retain and resume the command session until it returns an exit result. Finish each required gate before ending the task. Never pretend a completion notification will resume you, and never start another gate while the current one is running.` : `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and tracked background work holds the workspace awake for up to 45 minutes. Inspect any gate approaching that cap. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth. A pod restart loses both foreground and background processes. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
|
|
6343
|
-
runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- For a finite background gate, end your turn and its completion notification re-invokes you within the bounded liveness window. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn.${agentMode === "review" ? "" : " For CI on a pushed commit the shape is `wait_for_checks`, then end the turn (the pod may sleep; the GitHub result wakes it)."} Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 a deploy, a remote queue \u2014 never for CI, and never as insurance against a background job's own notification, which does fire.`,
|
|
6353
|
+
runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- For a finite background gate, end your turn and its completion notification re-invokes you within the bounded liveness window. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn.${agentMode === "review" ? "" : " For CI on a pushed commit the shape is `wait_for_checks`, then end the turn (the pod may sleep; the GitHub result wakes it)."} Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 a deploy, a remote queue \u2014 never for CI, and never as insurance against a background job's own notification, which does fire. One armed wakeup keeps this workspace awake until it fires (at most one hour), so arm exactly one and end the turn \u2014 a second one "as insurance" buys nothing.`,
|
|
6344
6354
|
`
|
|
6345
6355
|
Git:`,
|
|
6346
6356
|
`- Stay on \`${context.githubBranch}\` for the whole task: do not check out another branch and do not create one. It was cut from \`${context.baseBranch}\`, and PRs target that automatically.`,
|
|
@@ -6860,7 +6870,7 @@ var SEARCH_STATUSES = [
|
|
|
6860
6870
|
"Complete",
|
|
6861
6871
|
"Cancelled"
|
|
6862
6872
|
];
|
|
6863
|
-
var SEARCH_CARD_TYPES = ["task", "incident", "suggestion"];
|
|
6873
|
+
var SEARCH_CARD_TYPES = ["task", "incident", "suggestion", "decision"];
|
|
6864
6874
|
var searchFilterFields = {
|
|
6865
6875
|
tagNames: f.optional(
|
|
6866
6876
|
f.array(f.string(), {
|
|
@@ -7502,6 +7512,11 @@ var uploadAttachmentContract = defineToolContract({
|
|
|
7502
7512
|
title: f.optional(
|
|
7503
7513
|
f.string({ desc: "Short caption posted with the file (defaults to the file name)" })
|
|
7504
7514
|
),
|
|
7515
|
+
comment: f.optional(
|
|
7516
|
+
f.string({
|
|
7517
|
+
desc: "Alias of `title` (the external conveyor-mcp surface names this field `comment`). Provide at most one of the two."
|
|
7518
|
+
})
|
|
7519
|
+
),
|
|
7505
7520
|
tags: attachmentTags
|
|
7506
7521
|
}
|
|
7507
7522
|
},
|
|
@@ -7514,6 +7529,11 @@ var uploadAttachmentContract = defineToolContract({
|
|
|
7514
7529
|
comment: f.optional(
|
|
7515
7530
|
f.string({ desc: "When set, also posts the attachment to the task chat with this text" })
|
|
7516
7531
|
),
|
|
7532
|
+
title: f.optional(
|
|
7533
|
+
f.string({
|
|
7534
|
+
desc: "Alias of `comment` (the in-pod agent surface names this field `title`). Provide at most one of the two."
|
|
7535
|
+
})
|
|
7536
|
+
),
|
|
7517
7537
|
tags: attachmentTags,
|
|
7518
7538
|
mimeType: f.optional(
|
|
7519
7539
|
f.string({ desc: "Override the mime type inferred from the file extension" })
|
|
@@ -7553,6 +7573,293 @@ var createSuggestionContract = defineToolContract({
|
|
|
7553
7573
|
}
|
|
7554
7574
|
});
|
|
7555
7575
|
var suggestionsContracts = [createSuggestionContract];
|
|
7576
|
+
var OPTION_KINDS = ["current", "planned", "proposed"];
|
|
7577
|
+
var STATUS_LABELS = ["Open", "Decided", "Cancelled"];
|
|
7578
|
+
var optionDescription = "What this option means in the product, written as GIVEN / WHEN / THEN. Concrete enough that a reader six months from now can tell which behaviour was chosen.";
|
|
7579
|
+
var optionsDesc = "Two or three options. More than three means the question is not framed yet \u2014 narrow it or split it. Mark the option describing what the code does today as kind: current.";
|
|
7580
|
+
var defaultOptionDesc = "1-based position of the option that wins if nobody decides by the deadline. Required: silence has to become a decision, never a stall.";
|
|
7581
|
+
var agentOption = f.object(
|
|
7582
|
+
{
|
|
7583
|
+
label: f.string({ min: 1, desc: "Short name for the option, under 80 characters" }),
|
|
7584
|
+
description: f.string({ min: 1, desc: optionDescription }),
|
|
7585
|
+
kind: f.optional(
|
|
7586
|
+
f.enum(OPTION_KINDS, {
|
|
7587
|
+
desc: 'current = what the code does today, planned = already agreed but unbuilt, proposed = new. Defaults to "proposed".'
|
|
7588
|
+
})
|
|
7589
|
+
)
|
|
7590
|
+
},
|
|
7591
|
+
{ desc: "One option people can choose" }
|
|
7592
|
+
);
|
|
7593
|
+
var createDecisionContract = defineToolContract({
|
|
7594
|
+
name: "create_decision",
|
|
7595
|
+
agent: {
|
|
7596
|
+
description: `Raise a product decision: a fork the plan cannot be right without. Two or three options, one default, a deadline. Run list_decisions FIRST \u2014 an answered decision is the answer, and re-asking it wastes everybody's time. Do NOT file a decision for an assumption you can simply state: an assumed behaviour belongs in the plan as a GIVEN / WHEN / THEN line under "Assumed behaviours". Returns the decision in full (the same JSON as get_decision, with option ids and the deadline) plus which cards were linked.`,
|
|
7597
|
+
fields: {
|
|
7598
|
+
question: f.string({
|
|
7599
|
+
min: 1,
|
|
7600
|
+
desc: "The question, as one plain sentence ending in a question mark"
|
|
7601
|
+
}),
|
|
7602
|
+
summary: f.optional(
|
|
7603
|
+
f.string({
|
|
7604
|
+
desc: cardDescriptionDesc("One or two sentences of context for the card face")
|
|
7605
|
+
})
|
|
7606
|
+
),
|
|
7607
|
+
context: f.string({
|
|
7608
|
+
min: 1,
|
|
7609
|
+
desc: "Markdown background: why this is being asked now, and what breaks if it is answered wrongly"
|
|
7610
|
+
}),
|
|
7611
|
+
options: f.array(agentOption, { min: 2, desc: optionsDesc }),
|
|
7612
|
+
default_option: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
|
|
7613
|
+
deadline_hours: f.optional(
|
|
7614
|
+
f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
|
|
7615
|
+
),
|
|
7616
|
+
impacted_cards: f.optional(
|
|
7617
|
+
f.array(f.string(), {
|
|
7618
|
+
desc: "Slugs or ids of cards this decision blocks. They cannot start until it is settled, and they are un-blocked the moment it is. Blocking a card needs Moderate access: without it the decision is still raised and each ref comes back under refused."
|
|
7619
|
+
})
|
|
7620
|
+
),
|
|
7621
|
+
include_current_card: f.optional(
|
|
7622
|
+
f.boolean({
|
|
7623
|
+
desc: "Also block the card this session is working on. Defaults to true \u2014 you are raising this because your own plan is stuck on it. Pass false to raise the question without stopping your own work."
|
|
7624
|
+
})
|
|
7625
|
+
),
|
|
7626
|
+
tag_names: f.optional(
|
|
7627
|
+
f.array(f.string(), {
|
|
7628
|
+
desc: "Tag names. The settled decision is written into each tag's overview, which is how the next agent finds it."
|
|
7629
|
+
})
|
|
7630
|
+
),
|
|
7631
|
+
raised_by_pr: f.optional(
|
|
7632
|
+
f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
|
|
7633
|
+
),
|
|
7634
|
+
raised_by_card: f.optional(
|
|
7635
|
+
f.string({
|
|
7636
|
+
desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impacted_cards to block a card."
|
|
7637
|
+
})
|
|
7638
|
+
),
|
|
7639
|
+
deadline_at: f.optional(
|
|
7640
|
+
f.string({
|
|
7641
|
+
desc: "Absolute deadline as an ISO 8601 timestamp, e.g. 2026-09-25T17:00:00Z. Wins over deadline_hours when both are given; must be in the future and within 720 hours."
|
|
7642
|
+
})
|
|
7643
|
+
)
|
|
7644
|
+
}
|
|
7645
|
+
},
|
|
7646
|
+
mcp: {
|
|
7647
|
+
description: "Raise a product decision on a Conveyor project: a fork that has to be settled before the work can be planned correctly. Two or three options, one default that applies if nobody decides, and a deadline. Search list_decisions first \u2014 a Decided decision is the answer. Pass projectId to target a specific project; otherwise the configured default project is used.",
|
|
7648
|
+
fields: {
|
|
7649
|
+
projectId: mcpProjectId,
|
|
7650
|
+
question: f.string({ desc: "The question, as one plain sentence" }),
|
|
7651
|
+
summary: f.optional(f.string({ desc: cardDescriptionDesc("Context for the card face") })),
|
|
7652
|
+
context: f.string({ desc: "Markdown background: why this is being asked now" }),
|
|
7653
|
+
options: f.array(agentOption, { min: 2, desc: optionsDesc }),
|
|
7654
|
+
defaultOption: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
|
|
7655
|
+
deadlineHours: f.optional(
|
|
7656
|
+
f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
|
|
7657
|
+
),
|
|
7658
|
+
impactedCards: f.optional(
|
|
7659
|
+
f.array(f.string(), {
|
|
7660
|
+
desc: "Slugs or ids of cards this decision blocks. Needs Moderate access; without it each ref comes back under refused and the decision is still raised."
|
|
7661
|
+
})
|
|
7662
|
+
),
|
|
7663
|
+
tagNames: f.optional(f.array(f.string(), { desc: "Tag names to categorize the decision" })),
|
|
7664
|
+
raisedByPr: f.optional(
|
|
7665
|
+
f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
|
|
7666
|
+
),
|
|
7667
|
+
raisedByCard: f.optional(
|
|
7668
|
+
f.string({
|
|
7669
|
+
desc: "Slug or id of the card that raised the question. A link back, not a block \u2014 use impactedCards to block a card."
|
|
7670
|
+
})
|
|
7671
|
+
),
|
|
7672
|
+
deadlineAt: f.optional(
|
|
7673
|
+
f.string({
|
|
7674
|
+
desc: "Absolute deadline (ISO 8601). Wins over deadlineHours when both are given; must be in the future and within 720 hours."
|
|
7675
|
+
})
|
|
7676
|
+
)
|
|
7677
|
+
}
|
|
7678
|
+
}
|
|
7679
|
+
});
|
|
7680
|
+
var listDecisionsContract = defineToolContract({
|
|
7681
|
+
name: "list_decisions",
|
|
7682
|
+
agent: {
|
|
7683
|
+
description: `Search this project's decisions. Run this BEFORE planning anything in an area: "have we already decided how X behaves?" Each row carries the summary, who raised it (and from which card or PR), when, and \u2014 once settled \u2014 the resolution, so the list doubles as the decision log. Filter by card to see what is blocking a specific card.`,
|
|
7684
|
+
fields: {
|
|
7685
|
+
status: f.optional(
|
|
7686
|
+
f.enum(STATUS_LABELS, {
|
|
7687
|
+
desc: "Open = still being voted on, Decided = settled, Cancelled = dropped"
|
|
7688
|
+
})
|
|
7689
|
+
),
|
|
7690
|
+
tag: f.optional(f.string({ desc: 'Tag name, e.g. "slack"' })),
|
|
7691
|
+
card: f.optional(
|
|
7692
|
+
f.string({ desc: "Slug or id of a card \u2014 returns the decisions it is blocked by" })
|
|
7693
|
+
),
|
|
7694
|
+
query: f.optional(f.string({ desc: "Text search on the question" })),
|
|
7695
|
+
limit: f.optional(f.number({ desc: "Max rows to return (default 50, max 200)" }))
|
|
7696
|
+
}
|
|
7697
|
+
},
|
|
7698
|
+
mcp: {
|
|
7699
|
+
description: `List and search a Conveyor project's decisions. Decided rows include their resolution, so this is the fastest way to answer "did we already settle this?". Pass projectId to target a specific project; otherwise the configured default project is used.`,
|
|
7700
|
+
fields: {
|
|
7701
|
+
projectId: mcpProjectId,
|
|
7702
|
+
status: f.optional(f.enum(STATUS_LABELS, { desc: "Filter by state" })),
|
|
7703
|
+
tag: f.optional(f.string({ desc: "Tag name to filter by" })),
|
|
7704
|
+
card: f.optional(f.string({ desc: "Card slug or id \u2014 decisions blocking that card" })),
|
|
7705
|
+
query: f.optional(f.string({ desc: "Text search on the question" })),
|
|
7706
|
+
limit: f.optional(
|
|
7707
|
+
f.number({ int: true, min: 1, max: 200, desc: "Max rows to return (default 50)" })
|
|
7708
|
+
)
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7711
|
+
});
|
|
7712
|
+
var getDecisionContract = defineToolContract({
|
|
7713
|
+
name: "get_decision",
|
|
7714
|
+
agent: {
|
|
7715
|
+
description: "Read one decision in full: its summary and context, who raised it and from where, every option with its GIVEN / WHEN / THEN, every vote with who cast it and why, the running tally, the cards it blocks, and the resolution once there is one. Use read_task_chat with the same id for the discussion thread.",
|
|
7716
|
+
fields: {
|
|
7717
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" })
|
|
7718
|
+
}
|
|
7719
|
+
},
|
|
7720
|
+
mcp: {
|
|
7721
|
+
description: "Read one Conveyor decision in full \u2014 context, options, votes with reasons, tally, impacted cards, and the resolution. Pass projectId to disambiguate a slug; otherwise the configured default project is used.",
|
|
7722
|
+
fields: {
|
|
7723
|
+
projectId: mcpProjectId,
|
|
7724
|
+
decision: f.string({ desc: "Decision slug or id" })
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7727
|
+
});
|
|
7728
|
+
var voteDecisionContract = defineToolContract({
|
|
7729
|
+
name: "vote_decision",
|
|
7730
|
+
agent: {
|
|
7731
|
+
description: "Vote on an open decision. One vote per voter: voting again replaces your previous one. Always give a reason \u2014 the resolution weighs reasons over counts, and a vote with no reason carries almost nothing.",
|
|
7732
|
+
fields: {
|
|
7733
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" }),
|
|
7734
|
+
option: f.string({ min: 1, desc: "Option id, or its 1-based position" }),
|
|
7735
|
+
reason: f.optional(
|
|
7736
|
+
f.string({ desc: "Why this option, in one or two sentences (max 255 characters)" })
|
|
7737
|
+
)
|
|
7738
|
+
}
|
|
7739
|
+
},
|
|
7740
|
+
mcp: {
|
|
7741
|
+
description: "Vote on an open Conveyor decision, with a reason. One vote per voter \u2014 voting again replaces the previous one.",
|
|
7742
|
+
fields: {
|
|
7743
|
+
projectId: mcpProjectId,
|
|
7744
|
+
decision: f.string({ desc: "Decision slug or id" }),
|
|
7745
|
+
option: f.string({ desc: "Option id, or its 1-based position" }),
|
|
7746
|
+
reason: f.optional(f.string({ desc: "Why this option (max 255 characters)" }))
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
});
|
|
7750
|
+
var resolveDecisionContract = defineToolContract({
|
|
7751
|
+
name: "resolve_decision",
|
|
7752
|
+
agent: {
|
|
7753
|
+
description: "Settle a decision. This is not a formality: the resolution is written into the plan of every card the decision blocked, into each of their chats, and into the overview of every tag it carries, and those cards become startable. Say what was decided in one or two sentences \u2014 a reader months later should not need to open the thread.",
|
|
7754
|
+
fields: {
|
|
7755
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" }),
|
|
7756
|
+
option: f.string({ min: 1, desc: "Winning option id, or its 1-based position" }),
|
|
7757
|
+
resolution: f.string({
|
|
7758
|
+
min: 1,
|
|
7759
|
+
desc: "What was decided, in one or two plain sentences (max 255 characters)"
|
|
7760
|
+
}),
|
|
7761
|
+
notes: f.optional(
|
|
7762
|
+
f.string({ desc: "Markdown reasoning. Link any verdict attachment here." })
|
|
7763
|
+
)
|
|
7764
|
+
}
|
|
7765
|
+
},
|
|
7766
|
+
mcp: {
|
|
7767
|
+
description: "Settle a Conveyor decision. The resolution is written into every impacted card's plan and chat and into its tags' overviews, and the impacted cards become startable.",
|
|
7768
|
+
fields: {
|
|
7769
|
+
projectId: mcpProjectId,
|
|
7770
|
+
decision: f.string({ desc: "Decision slug or id" }),
|
|
7771
|
+
option: f.string({ desc: "Winning option id, or its 1-based position" }),
|
|
7772
|
+
resolution: f.string({ desc: "What was decided (max 255 characters)" }),
|
|
7773
|
+
notes: f.optional(f.string({ desc: "Markdown reasoning" }))
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
});
|
|
7777
|
+
var updateDecisionContract = defineToolContract({
|
|
7778
|
+
name: "update_decision",
|
|
7779
|
+
agent: {
|
|
7780
|
+
description: "Change an open decision: fix the question, summary, context or tags, rewrite the options while nobody has voted, move the deadline, link or unlink the cards it blocks, or cancel it. Cancelling un-blocks the impacted cards without answering the question, so only do it when the question stopped mattering.",
|
|
7781
|
+
fields: {
|
|
7782
|
+
decision: f.string({ min: 1, desc: "Decision slug or id" }),
|
|
7783
|
+
question: f.optional(f.string({ min: 1, desc: "New question, one plain sentence" })),
|
|
7784
|
+
summary: f.optional(
|
|
7785
|
+
f.string({ desc: cardDescriptionDesc("New one- or two-sentence summary") })
|
|
7786
|
+
),
|
|
7787
|
+
context: f.optional(f.string({ min: 1, desc: "New markdown background" })),
|
|
7788
|
+
options: f.optional(
|
|
7789
|
+
f.array(agentOption, {
|
|
7790
|
+
min: 2,
|
|
7791
|
+
desc: "Replacement options. Accepted only while the decision has no votes \u2014 once somebody has voted, cancel it and raise a new one instead. Send default_option with it: the old default no longer exists."
|
|
7792
|
+
})
|
|
7793
|
+
),
|
|
7794
|
+
default_option: f.optional(
|
|
7795
|
+
f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
|
|
7796
|
+
),
|
|
7797
|
+
tag_names: f.optional(
|
|
7798
|
+
f.array(f.string(), { desc: "Replacement tag names (replaces the current set)" })
|
|
7799
|
+
),
|
|
7800
|
+
deadline_hours: f.optional(f.number({ desc: "New deadline, in hours from now. 1 to 720." })),
|
|
7801
|
+
deadline_at: f.optional(
|
|
7802
|
+
f.string({
|
|
7803
|
+
desc: "New absolute deadline (ISO 8601). Wins over deadline_hours; must be in the future and within 720 hours."
|
|
7804
|
+
})
|
|
7805
|
+
),
|
|
7806
|
+
add_impacted_cards: f.optional(
|
|
7807
|
+
f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
|
|
7808
|
+
),
|
|
7809
|
+
remove_impacted_cards: f.optional(
|
|
7810
|
+
f.array(f.string(), { desc: "Ids of cards to stop blocking" })
|
|
7811
|
+
),
|
|
7812
|
+
status: f.optional(
|
|
7813
|
+
f.enum(["Cancelled", "Open"], {
|
|
7814
|
+
desc: "Cancelled drops the question; Open reopens a settled one and clears its answer"
|
|
7815
|
+
})
|
|
7816
|
+
)
|
|
7817
|
+
}
|
|
7818
|
+
},
|
|
7819
|
+
mcp: {
|
|
7820
|
+
description: "Change an open Conveyor decision: fix the question, summary, context or tags, rewrite the options while nobody has voted, move the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
|
|
7821
|
+
fields: {
|
|
7822
|
+
projectId: mcpProjectId,
|
|
7823
|
+
decision: f.string({ desc: "Decision slug or id" }),
|
|
7824
|
+
question: f.optional(f.string({ desc: "New question" })),
|
|
7825
|
+
summary: f.optional(f.string({ desc: cardDescriptionDesc("New summary") })),
|
|
7826
|
+
context: f.optional(f.string({ desc: "New markdown background" })),
|
|
7827
|
+
options: f.optional(
|
|
7828
|
+
f.array(agentOption, {
|
|
7829
|
+
min: 2,
|
|
7830
|
+
desc: "Replacement options. Accepted only while the decision has no votes. Send defaultOption with it: the old default no longer exists."
|
|
7831
|
+
})
|
|
7832
|
+
),
|
|
7833
|
+
defaultOption: f.optional(
|
|
7834
|
+
f.number({ int: true, min: 1, max: 3, desc: "1-based position of the new default" })
|
|
7835
|
+
),
|
|
7836
|
+
tagNames: f.optional(f.array(f.string(), { desc: "Replacement tag names" })),
|
|
7837
|
+
deadlineHours: f.optional(
|
|
7838
|
+
f.number({ min: 1, max: 720, desc: "New deadline, in hours from now" })
|
|
7839
|
+
),
|
|
7840
|
+
deadlineAt: f.optional(
|
|
7841
|
+
f.string({ desc: "New absolute deadline (ISO 8601). Wins over deadlineHours." })
|
|
7842
|
+
),
|
|
7843
|
+
addImpactedCards: f.optional(
|
|
7844
|
+
f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
|
|
7845
|
+
),
|
|
7846
|
+
removeImpactedCards: f.optional(
|
|
7847
|
+
f.array(f.string(), { desc: "Ids of cards to stop blocking" })
|
|
7848
|
+
),
|
|
7849
|
+
status: f.optional(
|
|
7850
|
+
f.enum(["Cancelled", "Open"], { desc: "Cancel it, or reopen a settled one" })
|
|
7851
|
+
)
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
});
|
|
7855
|
+
var decisionsContracts = [
|
|
7856
|
+
createDecisionContract,
|
|
7857
|
+
listDecisionsContract,
|
|
7858
|
+
getDecisionContract,
|
|
7859
|
+
voteDecisionContract,
|
|
7860
|
+
resolveDecisionContract,
|
|
7861
|
+
updateDecisionContract
|
|
7862
|
+
];
|
|
7556
7863
|
var createPullRequestContract = defineToolContract({
|
|
7557
7864
|
name: "create_pull_request",
|
|
7558
7865
|
agent: {
|
|
@@ -8213,6 +8520,7 @@ var TOOL_CONTRACTS = Object.fromEntries(
|
|
|
8213
8520
|
...subtasksContracts,
|
|
8214
8521
|
...attachmentsContracts,
|
|
8215
8522
|
...suggestionsContracts,
|
|
8523
|
+
...decisionsContracts,
|
|
8216
8524
|
...pullRequestContracts,
|
|
8217
8525
|
...integrationsContracts,
|
|
8218
8526
|
...driveContracts,
|
|
@@ -8276,10 +8584,11 @@ function buildReadTaskChatTool(connection) {
|
|
|
8276
8584
|
taskId: task_id
|
|
8277
8585
|
});
|
|
8278
8586
|
return textResult(JSON.stringify(messages, null, 2));
|
|
8279
|
-
} catch {
|
|
8587
|
+
} catch (error) {
|
|
8280
8588
|
return textResult(
|
|
8281
8589
|
JSON.stringify({
|
|
8282
|
-
|
|
8590
|
+
error: `Failed to read chat: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
8591
|
+
note: "Usually transient \u2014 retry. Chat history up to this session's start was provided in the initial context."
|
|
8283
8592
|
})
|
|
8284
8593
|
);
|
|
8285
8594
|
}
|
|
@@ -9063,7 +9372,8 @@ Paste this into the PR description to show it inline:
|
|
|
9063
9372
|
${snippet}`;
|
|
9064
9373
|
}
|
|
9065
9374
|
function buildUploadAttachmentTool(connection, config) {
|
|
9066
|
-
return defineContractTool(uploadAttachmentContract, async ({ path, title, tags }) => {
|
|
9375
|
+
return defineContractTool(uploadAttachmentContract, async ({ path, title, comment, tags }) => {
|
|
9376
|
+
const caption = title ?? comment;
|
|
9067
9377
|
try {
|
|
9068
9378
|
const filePath = isAbsolute(path) ? path : join16(config.workspaceDir, path);
|
|
9069
9379
|
const mimeType = inferMimeType(filePath);
|
|
@@ -9099,11 +9409,11 @@ function buildUploadAttachmentTool(connection, config) {
|
|
|
9099
9409
|
const result = await connection.call("confirmFileUpload", {
|
|
9100
9410
|
sessionId: connection.sessionId,
|
|
9101
9411
|
fileId,
|
|
9102
|
-
title,
|
|
9412
|
+
title: caption,
|
|
9103
9413
|
tags
|
|
9104
9414
|
});
|
|
9105
9415
|
return textResult(
|
|
9106
|
-
`Uploaded ${fileName} (${info.size} bytes) and posted it to the task chat${
|
|
9416
|
+
`Uploaded ${fileName} (${info.size} bytes) and posted it to the task chat${caption ? ` with caption "${caption}"` : ""}. File ID: ${result.fileId}${describeTags(result.appliedTags, result.unknownTags)}${describeEmbed(fileName, result.downloadUrl)}`
|
|
9107
9417
|
);
|
|
9108
9418
|
} catch (error) {
|
|
9109
9419
|
return textResult(
|
|
@@ -10465,6 +10775,155 @@ ${res.map(describeItem).join("\n")}`);
|
|
|
10465
10775
|
];
|
|
10466
10776
|
}
|
|
10467
10777
|
|
|
10778
|
+
// src/tools/decision-tools.ts
|
|
10779
|
+
function errText5(prefix, error) {
|
|
10780
|
+
return textResult(`${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
10781
|
+
}
|
|
10782
|
+
function buildDecisionTools(connection, projectId) {
|
|
10783
|
+
return [
|
|
10784
|
+
...readTools2(connection, projectId),
|
|
10785
|
+
...raiseAndVoteTools(connection, projectId),
|
|
10786
|
+
...settleTools(connection, projectId)
|
|
10787
|
+
];
|
|
10788
|
+
}
|
|
10789
|
+
function readTools2(connection, projectId) {
|
|
10790
|
+
return [
|
|
10791
|
+
defineContractTool(
|
|
10792
|
+
listDecisionsContract,
|
|
10793
|
+
async (input) => {
|
|
10794
|
+
try {
|
|
10795
|
+
const res = await connection.call("listProjectDecisions", {
|
|
10796
|
+
projectId,
|
|
10797
|
+
status: input.status,
|
|
10798
|
+
tag: input.tag,
|
|
10799
|
+
card: input.card,
|
|
10800
|
+
query: input.query,
|
|
10801
|
+
limit: input.limit
|
|
10802
|
+
});
|
|
10803
|
+
if (res.decisions.length === 0) {
|
|
10804
|
+
return textResult(
|
|
10805
|
+
"No decisions match. Nothing has been settled here, so you are free to choose \u2014 state the choice in the plan as a GIVEN / WHEN / THEN assumption, or raise a decision with create_decision if the plan cannot be right without an answer."
|
|
10806
|
+
);
|
|
10807
|
+
}
|
|
10808
|
+
return textResult(JSON.stringify(res.decisions, null, 2));
|
|
10809
|
+
} catch (error) {
|
|
10810
|
+
return errText5("Failed to list decisions", error);
|
|
10811
|
+
}
|
|
10812
|
+
},
|
|
10813
|
+
{ annotations: { readOnlyHint: true } }
|
|
10814
|
+
),
|
|
10815
|
+
defineContractTool(
|
|
10816
|
+
getDecisionContract,
|
|
10817
|
+
async (input) => {
|
|
10818
|
+
try {
|
|
10819
|
+
const res = await connection.call("getProjectDecision", {
|
|
10820
|
+
projectId,
|
|
10821
|
+
decision: input.decision
|
|
10822
|
+
});
|
|
10823
|
+
return textResult(JSON.stringify(res, null, 2));
|
|
10824
|
+
} catch (error) {
|
|
10825
|
+
return errText5("Failed to read the decision", error);
|
|
10826
|
+
}
|
|
10827
|
+
},
|
|
10828
|
+
{ annotations: { readOnlyHint: true } }
|
|
10829
|
+
)
|
|
10830
|
+
];
|
|
10831
|
+
}
|
|
10832
|
+
function raiseAndVoteTools(connection, projectId) {
|
|
10833
|
+
return [
|
|
10834
|
+
defineContractTool(createDecisionContract, async (input) => {
|
|
10835
|
+
try {
|
|
10836
|
+
const res = await connection.call("createProjectDecision", {
|
|
10837
|
+
projectId,
|
|
10838
|
+
question: input.question,
|
|
10839
|
+
summary: input.summary,
|
|
10840
|
+
context: input.context,
|
|
10841
|
+
options: input.options,
|
|
10842
|
+
defaultOption: input.default_option,
|
|
10843
|
+
deadlineHours: input.deadline_hours,
|
|
10844
|
+
impactedCards: input.impacted_cards,
|
|
10845
|
+
includeCurrentCard: input.include_current_card,
|
|
10846
|
+
sessionId: connection.sessionId,
|
|
10847
|
+
tagNames: input.tag_names,
|
|
10848
|
+
raisedByPr: input.raised_by_pr,
|
|
10849
|
+
raisedByCard: input.raised_by_card,
|
|
10850
|
+
deadlineAt: input.deadline_at
|
|
10851
|
+
});
|
|
10852
|
+
const { impactedLinked, refused, ...detail } = res;
|
|
10853
|
+
const refusedLine = refused.length > 0 ? `
|
|
10854
|
+
|
|
10855
|
+
Not linked: ${refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
|
|
10856
|
+
return textResult(
|
|
10857
|
+
`Raised decision ${res.slug} \u2014 ${res.url}
|
|
10858
|
+
Blocking ${impactedLinked.length} card(s).${refusedLine}
|
|
10859
|
+
|
|
10860
|
+
${JSON.stringify(detail, null, 2)}`
|
|
10861
|
+
);
|
|
10862
|
+
} catch (error) {
|
|
10863
|
+
return errText5("Failed to raise the decision", error);
|
|
10864
|
+
}
|
|
10865
|
+
}),
|
|
10866
|
+
defineContractTool(voteDecisionContract, async (input) => {
|
|
10867
|
+
try {
|
|
10868
|
+
const res = await connection.call("voteProjectDecision", {
|
|
10869
|
+
projectId,
|
|
10870
|
+
decision: input.decision,
|
|
10871
|
+
option: input.option,
|
|
10872
|
+
reason: input.reason
|
|
10873
|
+
});
|
|
10874
|
+
const tally = res.options.map((o) => `${o.label}: ${o.votes}`).join(", ");
|
|
10875
|
+
return textResult(`Voted on ${res.slug}. Tally \u2014 ${tally}.`);
|
|
10876
|
+
} catch (error) {
|
|
10877
|
+
return errText5("Failed to vote", error);
|
|
10878
|
+
}
|
|
10879
|
+
})
|
|
10880
|
+
];
|
|
10881
|
+
}
|
|
10882
|
+
function settleTools(connection, projectId) {
|
|
10883
|
+
return [
|
|
10884
|
+
defineContractTool(resolveDecisionContract, async (input) => {
|
|
10885
|
+
try {
|
|
10886
|
+
const res = await connection.call("resolveProjectDecision", {
|
|
10887
|
+
projectId,
|
|
10888
|
+
decision: input.decision,
|
|
10889
|
+
option: input.option,
|
|
10890
|
+
resolution: input.resolution,
|
|
10891
|
+
notes: input.notes
|
|
10892
|
+
});
|
|
10893
|
+
return textResult(
|
|
10894
|
+
`Settled ${res.slug} as "${res.chosenOption}". ${res.impactedCount} card(s) were written back to and are no longer blocked.`
|
|
10895
|
+
);
|
|
10896
|
+
} catch (error) {
|
|
10897
|
+
return errText5("Failed to settle the decision", error);
|
|
10898
|
+
}
|
|
10899
|
+
}),
|
|
10900
|
+
defineContractTool(updateDecisionContract, async (input) => {
|
|
10901
|
+
try {
|
|
10902
|
+
const res = await connection.call("updateProjectDecision", {
|
|
10903
|
+
projectId,
|
|
10904
|
+
decision: input.decision,
|
|
10905
|
+
question: input.question,
|
|
10906
|
+
summary: input.summary,
|
|
10907
|
+
context: input.context,
|
|
10908
|
+
options: input.options,
|
|
10909
|
+
defaultOption: input.default_option,
|
|
10910
|
+
tagNames: input.tag_names,
|
|
10911
|
+
deadlineHours: input.deadline_hours,
|
|
10912
|
+
deadlineAt: input.deadline_at,
|
|
10913
|
+
addImpactedCards: input.add_impacted_cards,
|
|
10914
|
+
removeImpactedCards: input.remove_impacted_cards,
|
|
10915
|
+
status: input.status
|
|
10916
|
+
});
|
|
10917
|
+
return textResult(
|
|
10918
|
+
`Updated ${res.slug} \u2014 ${res.status}, ${res.impactedCount} impacted card(s), deadline ${res.deadlineAt}.`
|
|
10919
|
+
);
|
|
10920
|
+
} catch (error) {
|
|
10921
|
+
return errText5("Failed to update the decision", error);
|
|
10922
|
+
}
|
|
10923
|
+
})
|
|
10924
|
+
];
|
|
10925
|
+
}
|
|
10926
|
+
|
|
10468
10927
|
// src/tools/connected-tools.ts
|
|
10469
10928
|
function connectedToolsFor(connection, context) {
|
|
10470
10929
|
const projectId = context?.projectId;
|
|
@@ -10474,6 +10933,9 @@ function connectedToolsFor(connection, context) {
|
|
|
10474
10933
|
buildListProjectIntegrationsTool(connection, projectId),
|
|
10475
10934
|
...context.chatChannelsConfigured ? buildChannelTools(connection, projectId) : [],
|
|
10476
10935
|
...context.meetingsAvailable ? buildMeetingTools(connection, projectId) : [],
|
|
10936
|
+
// Always available: the decision tables exist on every project, and a fork
|
|
10937
|
+
// an agent cannot file is a fork it assumes its way past instead.
|
|
10938
|
+
...buildDecisionTools(connection, projectId),
|
|
10477
10939
|
...context.googleAnalyticsConfigured ? [buildAnalyticsSummaryTool(connection, projectId)] : [],
|
|
10478
10940
|
...context.gcpLogsConfigured ? [buildQueryGcpLogsTool(connection, projectId)] : [],
|
|
10479
10941
|
...context.grafanaLogsConfigured ? [buildQueryGrafanaLogsTool(connection, projectId)] : []
|
|
@@ -10846,6 +11308,26 @@ function safeVoid(promise, context) {
|
|
|
10846
11308
|
});
|
|
10847
11309
|
}
|
|
10848
11310
|
}
|
|
11311
|
+
function addTurnUsage(total, usage) {
|
|
11312
|
+
const next = {
|
|
11313
|
+
inputTokens: total.inputTokens + (usage.input_tokens ?? 0),
|
|
11314
|
+
outputTokens: total.outputTokens + (usage.output_tokens ?? 0),
|
|
11315
|
+
cacheReadInputTokens: total.cacheReadInputTokens + (usage.cache_read_input_tokens ?? 0),
|
|
11316
|
+
cacheCreationInputTokens: total.cacheCreationInputTokens + (usage.cache_creation_input_tokens ?? 0),
|
|
11317
|
+
totalTokens: 0
|
|
11318
|
+
};
|
|
11319
|
+
next.totalTokens = next.inputTokens + next.outputTokens + next.cacheReadInputTokens + next.cacheCreationInputTokens;
|
|
11320
|
+
return next;
|
|
11321
|
+
}
|
|
11322
|
+
function emptyTurnUsage() {
|
|
11323
|
+
return {
|
|
11324
|
+
inputTokens: 0,
|
|
11325
|
+
outputTokens: 0,
|
|
11326
|
+
cacheReadInputTokens: 0,
|
|
11327
|
+
cacheCreationInputTokens: 0,
|
|
11328
|
+
totalTokens: 0
|
|
11329
|
+
};
|
|
11330
|
+
}
|
|
10849
11331
|
function epochSecondsToISO(value) {
|
|
10850
11332
|
if (typeof value === "string") return value;
|
|
10851
11333
|
if (typeof value !== "number" || value <= 0) return void 0;
|
|
@@ -10927,11 +11409,16 @@ function emitContextUpdate(modelUsage, host, context, lastAssistantUsage) {
|
|
|
10927
11409
|
});
|
|
10928
11410
|
}
|
|
10929
11411
|
}
|
|
10930
|
-
function handleSuccessResult(event, host, context, startTime, lastAssistantUsage) {
|
|
11412
|
+
function handleSuccessResult(event, host, context, startTime, lastAssistantUsage, turnUsage) {
|
|
10931
11413
|
const durationMs = Date.now() - startTime;
|
|
10932
11414
|
const summary = event.result || "Task completed.";
|
|
10933
11415
|
const retriable = isRetriableMessage(summary);
|
|
10934
|
-
host.connection.sendEvent({
|
|
11416
|
+
host.connection.sendEvent({
|
|
11417
|
+
type: "completed",
|
|
11418
|
+
summary,
|
|
11419
|
+
durationMs,
|
|
11420
|
+
...turnUsage && turnUsage.totalTokens > 0 ? { usage: turnUsage } : {}
|
|
11421
|
+
});
|
|
10935
11422
|
const { modelUsage } = event;
|
|
10936
11423
|
if (modelUsage && typeof modelUsage === "object") {
|
|
10937
11424
|
emitContextUpdate(modelUsage, host, context, lastAssistantUsage);
|
|
@@ -10952,7 +11439,7 @@ function handleErrorResult(event, host) {
|
|
|
10952
11439
|
host.connection.sendEvent({ type: "error", message: errorMsg });
|
|
10953
11440
|
return { retriable };
|
|
10954
11441
|
}
|
|
10955
|
-
function handleResultEvent(event, host, context, startTime, lastAssistantUsage) {
|
|
11442
|
+
function handleResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage) {
|
|
10956
11443
|
const resultSummary = event.subtype === "success" ? event.result : event.errors.join(", ");
|
|
10957
11444
|
if (event.subtype === "success") {
|
|
10958
11445
|
const result2 = handleSuccessResult(
|
|
@@ -10960,15 +11447,16 @@ function handleResultEvent(event, host, context, startTime, lastAssistantUsage)
|
|
|
10960
11447
|
host,
|
|
10961
11448
|
context,
|
|
10962
11449
|
startTime,
|
|
10963
|
-
lastAssistantUsage
|
|
11450
|
+
lastAssistantUsage,
|
|
11451
|
+
turnUsage
|
|
10964
11452
|
);
|
|
10965
11453
|
return { ...result2, resultSummary };
|
|
10966
11454
|
}
|
|
10967
11455
|
const result = handleErrorResult(event, host);
|
|
10968
11456
|
return { ...result, resultSummary };
|
|
10969
11457
|
}
|
|
10970
|
-
async function emitResultEvent(event, host, context, startTime, lastAssistantUsage) {
|
|
10971
|
-
const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage);
|
|
11458
|
+
async function emitResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage) {
|
|
11459
|
+
const result = handleResultEvent(event, host, context, startTime, lastAssistantUsage, turnUsage);
|
|
10972
11460
|
const durationMs = Date.now() - startTime;
|
|
10973
11461
|
if (event.subtype === "success") {
|
|
10974
11462
|
const successEvent = event;
|
|
@@ -10976,7 +11464,8 @@ async function emitResultEvent(event, host, context, startTime, lastAssistantUsa
|
|
|
10976
11464
|
await host.callbacks.onEvent({
|
|
10977
11465
|
type: "completed",
|
|
10978
11466
|
summary,
|
|
10979
|
-
durationMs
|
|
11467
|
+
durationMs,
|
|
11468
|
+
...turnUsage && turnUsage.totalTokens > 0 ? { usage: turnUsage } : {}
|
|
10980
11469
|
});
|
|
10981
11470
|
} else if (!result.staleSession) {
|
|
10982
11471
|
const errorEvent = event;
|
|
@@ -11082,13 +11571,20 @@ async function handleAssistantCase(event, host, turnToolCalls) {
|
|
|
11082
11571
|
const msgUsage = event.message.usage;
|
|
11083
11572
|
return msgUsage ?? void 0;
|
|
11084
11573
|
}
|
|
11085
|
-
async function handleResultCase(event, host, context, startTime, isTyping, lastAssistantUsage) {
|
|
11574
|
+
async function handleResultCase(event, host, context, startTime, isTyping, lastAssistantUsage, turnUsage) {
|
|
11086
11575
|
let stoppedTyping = false;
|
|
11087
11576
|
if (isTyping) {
|
|
11088
11577
|
host.connection.sendTypingStop();
|
|
11089
11578
|
stoppedTyping = true;
|
|
11090
11579
|
}
|
|
11091
|
-
const resultInfo = await emitResultEvent(
|
|
11580
|
+
const resultInfo = await emitResultEvent(
|
|
11581
|
+
event,
|
|
11582
|
+
host,
|
|
11583
|
+
context,
|
|
11584
|
+
startTime,
|
|
11585
|
+
lastAssistantUsage,
|
|
11586
|
+
turnUsage
|
|
11587
|
+
);
|
|
11092
11588
|
return {
|
|
11093
11589
|
retriable: resultInfo.retriable,
|
|
11094
11590
|
resultSummary: resultInfo.resultSummary,
|
|
@@ -11192,7 +11688,10 @@ async function processAssistantCase(event, host, state) {
|
|
|
11192
11688
|
state.isTyping = true;
|
|
11193
11689
|
}
|
|
11194
11690
|
const usage = await handleAssistantCase(event, host, state.turnToolCalls);
|
|
11195
|
-
if (usage)
|
|
11691
|
+
if (usage) {
|
|
11692
|
+
state.lastAssistantUsage = usage;
|
|
11693
|
+
state.turnUsage = addTurnUsage(state.turnUsage, usage);
|
|
11694
|
+
}
|
|
11196
11695
|
if (!state.sawApiError || event.isApiError && !state.terminalApiError) {
|
|
11197
11696
|
const fullText = event.message.content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
11198
11697
|
if (API_ERROR_PATTERN.test(fullText)) {
|
|
@@ -11211,7 +11710,8 @@ async function processResultCase(event, host, context, startTime, state) {
|
|
|
11211
11710
|
context,
|
|
11212
11711
|
startTime,
|
|
11213
11712
|
state.isTyping,
|
|
11214
|
-
state.lastAssistantUsage
|
|
11713
|
+
state.lastAssistantUsage,
|
|
11714
|
+
state.turnUsage
|
|
11215
11715
|
);
|
|
11216
11716
|
if (info.stoppedTyping) state.isTyping = false;
|
|
11217
11717
|
state.retriable = info.retriable;
|
|
@@ -11244,6 +11744,7 @@ async function processEvents(events, context, host) {
|
|
|
11244
11744
|
staleSession: void 0,
|
|
11245
11745
|
authError: void 0,
|
|
11246
11746
|
lastAssistantUsage: void 0,
|
|
11747
|
+
turnUsage: emptyTurnUsage(),
|
|
11247
11748
|
turnToolCalls: [],
|
|
11248
11749
|
questionPending: false
|
|
11249
11750
|
};
|
|
@@ -11323,6 +11824,9 @@ function fallbackResetIso(rateLimitType, now = Date.now()) {
|
|
|
11323
11824
|
return new Date(now + (weekly ? TWENTY_FOUR_HOURS_MS : FIVE_HOURS_MS)).toISOString();
|
|
11324
11825
|
}
|
|
11325
11826
|
|
|
11827
|
+
// src/execution/tool-access.ts
|
|
11828
|
+
import { isAbsolute as isAbsolute3, posix } from "path";
|
|
11829
|
+
|
|
11326
11830
|
// src/execution/task-property-utils.ts
|
|
11327
11831
|
function collectMissingProps(taskProps) {
|
|
11328
11832
|
const missing = [];
|
|
@@ -11465,6 +11969,12 @@ function isPlanFile(input) {
|
|
|
11465
11969
|
const filePath = String(input.file_path ?? input.path ?? "");
|
|
11466
11970
|
return filePath.includes(".claude/plans/");
|
|
11467
11971
|
}
|
|
11972
|
+
function isScratchPath(input) {
|
|
11973
|
+
const filePath = String(input.file_path ?? input.path ?? "");
|
|
11974
|
+
if (!isAbsolute3(filePath)) return false;
|
|
11975
|
+
return posix.normalize(filePath).startsWith("/tmp/");
|
|
11976
|
+
}
|
|
11977
|
+
var ARTIFACT_DENIED = "Publish deliverables on the card with upload_attachment; the Artifact tool is not available in Conveyor sessions.";
|
|
11468
11978
|
var READ_ONLY_BLOCKED_BASH = [
|
|
11469
11979
|
{ name: "git commit", re: /\bgit\s+(?:-\S+\s+)*commit\b/ },
|
|
11470
11980
|
{ name: "git push", re: /\bgit\s+(?:-\S+\s+)*push\b/ },
|
|
@@ -11483,17 +11993,19 @@ function matchesReadOnlyBlocked(cmd) {
|
|
|
11483
11993
|
}
|
|
11484
11994
|
var POST_CHANNEL_TOOL = /(^|__)post_channel_message$/;
|
|
11485
11995
|
var CHANNEL_POST_DENIED = "Posting into a chat channel is not available in this mode. Reading channels still is \u2014 use read_channel_messages if you need the discussion.";
|
|
11996
|
+
var DECISION_SETTLE_TOOLS = /(^|__)(resolve_decision|update_decision)$/;
|
|
11997
|
+
var DECISION_SETTLE_DENIED = "Settling a decision is not available in review mode. Say on the PR that the change turns on an unsettled decision, and cite it with list_decisions \u2014 a reviewer does not make the product call.";
|
|
11486
11998
|
function handleReadOnlyToolAccess(toolName, input) {
|
|
11487
11999
|
if (POST_CHANNEL_TOOL.test(toolName)) {
|
|
11488
12000
|
return { behavior: "deny", message: CHANNEL_POST_DENIED };
|
|
11489
12001
|
}
|
|
11490
12002
|
if (PM_PLAN_FILE_TOOLS.has(toolName)) {
|
|
11491
|
-
if (isPlanFile(input)) {
|
|
12003
|
+
if (isPlanFile(input) || isScratchPath(input)) {
|
|
11492
12004
|
return { behavior: "allow", updatedInput: input };
|
|
11493
12005
|
}
|
|
11494
12006
|
return {
|
|
11495
12007
|
behavior: "deny",
|
|
11496
|
-
message: "Discovery mode is read-only. File writes are restricted to plan files."
|
|
12008
|
+
message: "Discovery mode is read-only. File writes are restricted to plan files and /tmp/ scratch files \u2014 write a deliverable under /tmp/ and attach it with upload_attachment."
|
|
11497
12009
|
};
|
|
11498
12010
|
}
|
|
11499
12011
|
if (toolName === "Bash") {
|
|
@@ -11532,6 +12044,9 @@ function handleReviewToolAccess(toolName, input) {
|
|
|
11532
12044
|
if (POST_CHANNEL_TOOL.test(toolName)) {
|
|
11533
12045
|
return { behavior: "deny", message: CHANNEL_POST_DENIED };
|
|
11534
12046
|
}
|
|
12047
|
+
if (DECISION_SETTLE_TOOLS.test(toolName)) {
|
|
12048
|
+
return { behavior: "deny", message: DECISION_SETTLE_DENIED };
|
|
12049
|
+
}
|
|
11535
12050
|
return handleBuildingToolAccess(toolName, input);
|
|
11536
12051
|
}
|
|
11537
12052
|
function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask) {
|
|
@@ -11649,7 +12164,7 @@ var DENIAL_FORCE_STOP_THRESHOLD = 8;
|
|
|
11649
12164
|
function handleDenialEscalation(host, consecutiveDenials) {
|
|
11650
12165
|
if (consecutiveDenials === DENIAL_WARNING_THRESHOLD) {
|
|
11651
12166
|
host.connection.postChatMessage(
|
|
11652
|
-
`\u26A0\uFE0F Multiple tool denials detected. You are in ${host.agentMode} mode \u2014 file writes outside .claude/plans/ are not permitted. Focus on creating a plan instead of implementing code changes.`
|
|
12167
|
+
`\u26A0\uFE0F Multiple tool denials detected. You are in ${host.agentMode} mode \u2014 file writes outside .claude/plans/ and /tmp/ are not permitted. Focus on creating a plan instead of implementing code changes.`
|
|
11653
12168
|
);
|
|
11654
12169
|
}
|
|
11655
12170
|
if (consecutiveDenials >= DENIAL_FORCE_STOP_THRESHOLD) {
|
|
@@ -11695,6 +12210,9 @@ function checkToolLoop(host, toolName, input) {
|
|
|
11695
12210
|
return { behavior: "deny", message: buildRepeatLoopMessage(repeats, isHeavyGateActive()) };
|
|
11696
12211
|
}
|
|
11697
12212
|
function resolveToolAccess(host, toolName, input) {
|
|
12213
|
+
if (toolName === "Artifact") {
|
|
12214
|
+
return { behavior: "deny", message: ARTIFACT_DENIED };
|
|
12215
|
+
}
|
|
11698
12216
|
switch (host.agentMode) {
|
|
11699
12217
|
case "discovery":
|
|
11700
12218
|
case "help":
|
|
@@ -13409,6 +13927,100 @@ var BackgroundWorkTracker = class {
|
|
|
13409
13927
|
}
|
|
13410
13928
|
};
|
|
13411
13929
|
|
|
13930
|
+
// src/runner/scheduled-wakeup.ts
|
|
13931
|
+
var SCHEDULE_WAKEUP_TOOL = "schedulewakeup";
|
|
13932
|
+
var SCHEDULED_WAKEUP_MIN_DELAY_SECONDS = 60;
|
|
13933
|
+
var SCHEDULED_WAKEUP_MAX_DELAY_SECONDS = 3600;
|
|
13934
|
+
var SCHEDULED_WAKEUP_GRACE_MS = 2 * 60 * 1e3;
|
|
13935
|
+
function asRecord3(input) {
|
|
13936
|
+
if (typeof input === "string") {
|
|
13937
|
+
try {
|
|
13938
|
+
const parsed = JSON.parse(input);
|
|
13939
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
13940
|
+
} catch {
|
|
13941
|
+
return null;
|
|
13942
|
+
}
|
|
13943
|
+
}
|
|
13944
|
+
return typeof input === "object" && input !== null ? input : null;
|
|
13945
|
+
}
|
|
13946
|
+
function parseScheduledWakeup(tool2, input, now = Date.now()) {
|
|
13947
|
+
if (tool2.toLowerCase() !== SCHEDULE_WAKEUP_TOOL) return null;
|
|
13948
|
+
const record = asRecord3(input);
|
|
13949
|
+
if (!record) return null;
|
|
13950
|
+
if (record.stop === true) return null;
|
|
13951
|
+
const raw = record.delaySeconds;
|
|
13952
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return null;
|
|
13953
|
+
const delaySeconds = Math.min(
|
|
13954
|
+
SCHEDULED_WAKEUP_MAX_DELAY_SECONDS,
|
|
13955
|
+
Math.max(SCHEDULED_WAKEUP_MIN_DELAY_SECONDS, raw)
|
|
13956
|
+
);
|
|
13957
|
+
return { delaySeconds, deadline: now + delaySeconds * 1e3 + SCHEDULED_WAKEUP_GRACE_MS };
|
|
13958
|
+
}
|
|
13959
|
+
function isScheduledWakeupStop(tool2, input) {
|
|
13960
|
+
if (tool2.toLowerCase() !== SCHEDULE_WAKEUP_TOOL) return false;
|
|
13961
|
+
return asRecord3(input)?.stop === true;
|
|
13962
|
+
}
|
|
13963
|
+
var ScheduledWakeupTracker = class {
|
|
13964
|
+
/** Absolute expiry (epoch ms) of the armed wakeup; null when none. */
|
|
13965
|
+
deadline = null;
|
|
13966
|
+
onChange;
|
|
13967
|
+
log;
|
|
13968
|
+
constructor(options = {}) {
|
|
13969
|
+
this.onChange = options.onChange;
|
|
13970
|
+
this.log = options.log ?? ((message) => process.stderr.write(message));
|
|
13971
|
+
}
|
|
13972
|
+
/**
|
|
13973
|
+
* Feed one `tool_use` from the turn stream. Returns true when it changed the
|
|
13974
|
+
* armed state (test/diagnostic signal; callers can ignore it).
|
|
13975
|
+
*/
|
|
13976
|
+
noteToolUse(tool2, input, now = Date.now()) {
|
|
13977
|
+
if (isScheduledWakeupStop(tool2, input)) {
|
|
13978
|
+
if (this.deadline === null) return false;
|
|
13979
|
+
this.deadline = null;
|
|
13980
|
+
this.log("[conveyor-agent] scheduled wakeup stopped \u2014 heartbeats may report idle again\n");
|
|
13981
|
+
this.onChange?.();
|
|
13982
|
+
return true;
|
|
13983
|
+
}
|
|
13984
|
+
const arm = parseScheduledWakeup(tool2, input, now);
|
|
13985
|
+
if (!arm) return false;
|
|
13986
|
+
this.deadline = arm.deadline;
|
|
13987
|
+
this.log(
|
|
13988
|
+
`[conveyor-agent] scheduled wakeup armed (${arm.delaySeconds}s) \u2014 heartbeats stay non-idle until it fires
|
|
13989
|
+
`
|
|
13990
|
+
);
|
|
13991
|
+
this.onChange?.();
|
|
13992
|
+
return true;
|
|
13993
|
+
}
|
|
13994
|
+
/** Is a non-expired wakeup armed? */
|
|
13995
|
+
hasPending(now = Date.now()) {
|
|
13996
|
+
this.prune(now);
|
|
13997
|
+
return this.deadline !== null;
|
|
13998
|
+
}
|
|
13999
|
+
/** Absolute expiry of the armed wakeup, or null when none is armed. Callers
|
|
14000
|
+
* bound their own deferrals by this rather than by a fixed cap — a one-hour
|
|
14001
|
+
* arm outlives `BACKGROUND_WORK_MAX_MS`. */
|
|
14002
|
+
pendingDeadline(now = Date.now()) {
|
|
14003
|
+
this.prune(now);
|
|
14004
|
+
return this.deadline;
|
|
14005
|
+
}
|
|
14006
|
+
/** Drop the arm (a turn started, or the session is shutting down). */
|
|
14007
|
+
clear() {
|
|
14008
|
+
if (this.deadline === null) return;
|
|
14009
|
+
this.deadline = null;
|
|
14010
|
+
this.onChange?.();
|
|
14011
|
+
}
|
|
14012
|
+
/** Expire a deadline that has passed, loudly — the wakeup should have fired
|
|
14013
|
+
* a turn by now, so a tripped expiry is worth seeing in the log. */
|
|
14014
|
+
prune(now) {
|
|
14015
|
+
if (this.deadline === null || this.deadline > now) return;
|
|
14016
|
+
this.deadline = null;
|
|
14017
|
+
this.log(
|
|
14018
|
+
"[conveyor-agent] scheduled wakeup deadline passed with no turn \u2014 reporting idle again\n"
|
|
14019
|
+
);
|
|
14020
|
+
this.onChange?.();
|
|
14021
|
+
}
|
|
14022
|
+
};
|
|
14023
|
+
|
|
13412
14024
|
// src/runner/live-children.ts
|
|
13413
14025
|
function findLiveChild(sources) {
|
|
13414
14026
|
for (const source of sources) {
|
|
@@ -13476,6 +14088,13 @@ var SessionRunner = class _SessionRunner {
|
|
|
13476
14088
|
backgroundWork = new BackgroundWorkTracker({
|
|
13477
14089
|
onChange: () => this.refreshLoopStatus()
|
|
13478
14090
|
});
|
|
14091
|
+
/** An armed `ScheduleWakeup` (the dynamic `/loop` mode): the turn ended, but
|
|
14092
|
+
* the agent asked to be re-invoked at a deadline. Same liveness contract as
|
|
14093
|
+
* `backgroundWork` — while it reports pending, heartbeats go out non-idle so
|
|
14094
|
+
* the reconciler cannot sleep the pod before the wakeup fires. */
|
|
14095
|
+
scheduledWakeup = new ScheduledWakeupTracker({
|
|
14096
|
+
onChange: () => this.refreshLoopStatus()
|
|
14097
|
+
});
|
|
13479
14098
|
/** Boot supervisor handle, set post-construction once it's known (it is
|
|
13480
14099
|
* started alongside connect(), before the runner itself exists in some
|
|
13481
14100
|
* call sites) — notified when the core loop goes live so it can release
|
|
@@ -13520,6 +14139,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
13520
14139
|
},
|
|
13521
14140
|
onIdleTimeout: () => {
|
|
13522
14141
|
if (this.deferShutdownForLiveChild("idle")) return;
|
|
14142
|
+
if (this.deferShutdownForScheduledWakeup("idle")) return;
|
|
13523
14143
|
if (this.deferShutdownForBackgroundWork()) return;
|
|
13524
14144
|
process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
|
|
13525
14145
|
this.stopped = true;
|
|
@@ -13532,6 +14152,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
13532
14152
|
},
|
|
13533
14153
|
onDormantTimeout: () => {
|
|
13534
14154
|
if (this.deferShutdownForLiveChild("dormant")) return;
|
|
14155
|
+
if (this.deferShutdownForScheduledWakeup("dormant")) return;
|
|
13535
14156
|
process.stderr.write("[conveyor-agent] Dormant idle timeout reached, shutting down\n");
|
|
13536
14157
|
this.stopped = true;
|
|
13537
14158
|
this.queryBridge?.stop();
|
|
@@ -13591,6 +14212,50 @@ var SessionRunner = class _SessionRunner {
|
|
|
13591
14212
|
const label = timer === "idle" ? "Idle" : "Dormant idle";
|
|
13592
14213
|
process.stderr.write(
|
|
13593
14214
|
`[conveyor-agent] ${label} timeout deferred: ${live.kind} session ${live.sessionId} active
|
|
14215
|
+
`
|
|
14216
|
+
);
|
|
14217
|
+
if (timer === "idle") {
|
|
14218
|
+
this.lifecycle.startIdleTimer(recheckMs);
|
|
14219
|
+
} else {
|
|
14220
|
+
this.dormantDeadline = Date.now() + recheckMs;
|
|
14221
|
+
this.lifecycle.startDormantTimer(recheckMs);
|
|
14222
|
+
}
|
|
14223
|
+
return true;
|
|
14224
|
+
}
|
|
14225
|
+
/**
|
|
14226
|
+
* Suppress an idle or dormant shutdown while a `ScheduleWakeup` is armed.
|
|
14227
|
+
*
|
|
14228
|
+
* Both timers matter. The idle timer (30 min) kills a loop that paces itself
|
|
14229
|
+
* slower than that, and the dormant timer (60 min after a `completed` event)
|
|
14230
|
+
* kills a one-hour arm — the agent declares completion at the end of every
|
|
14231
|
+
* tick, so a dynamic loop sits dormant by construction.
|
|
14232
|
+
*
|
|
14233
|
+
* The bound is the WAKEUP'S OWN deadline, not `BACKGROUND_WORK_MAX_MS`: the
|
|
14234
|
+
* tool clamps `delaySeconds` to one hour, which already exceeds that 45-min
|
|
14235
|
+
* cap, so reusing it would shut the pod down before the longest legal arm
|
|
14236
|
+
* fired. Once the deadline passes the tracker expires it and this returns
|
|
14237
|
+
* false, so a lost timer cannot pin a pod.
|
|
14238
|
+
*/
|
|
14239
|
+
deferShutdownForScheduledWakeup(timer) {
|
|
14240
|
+
let deadline;
|
|
14241
|
+
try {
|
|
14242
|
+
deadline = this.scheduledWakeup.pendingDeadline();
|
|
14243
|
+
} catch (err) {
|
|
14244
|
+
process.stderr.write(
|
|
14245
|
+
`[conveyor-agent] Scheduled-wakeup probe failed, not deferring: ${err}
|
|
14246
|
+
`
|
|
14247
|
+
);
|
|
14248
|
+
return false;
|
|
14249
|
+
}
|
|
14250
|
+
if (deadline === null) return false;
|
|
14251
|
+
const remainingMs = Math.max(0, deadline - Date.now());
|
|
14252
|
+
const recheckMs = Math.min(
|
|
14253
|
+
Math.max(remainingMs, 1),
|
|
14254
|
+
Math.max(this.lifecycle.config.idleTimeoutMs, 1)
|
|
14255
|
+
);
|
|
14256
|
+
const label = timer === "idle" ? "Idle" : "Dormant idle";
|
|
14257
|
+
process.stderr.write(
|
|
14258
|
+
`[conveyor-agent] ${label} timeout deferred: scheduled wakeup armed (${Math.round(remainingMs / 1e3)}s remaining)
|
|
13594
14259
|
`
|
|
13595
14260
|
);
|
|
13596
14261
|
if (timer === "idle") {
|
|
@@ -14520,6 +15185,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
14520
15185
|
});
|
|
14521
15186
|
this.portDiscovery.stop();
|
|
14522
15187
|
this.backgroundWork.clear();
|
|
15188
|
+
this.scheduledWakeup.clear();
|
|
14523
15189
|
this.loopLag.stop();
|
|
14524
15190
|
this.lifecycle.destroy();
|
|
14525
15191
|
this.connection.disconnect();
|
|
@@ -14760,6 +15426,7 @@ ${outcome.failures.join("\n")}
|
|
|
14760
15426
|
if (this.questionPendingSince !== null && Date.now() - this.questionPendingSince < resolveQuestionGraceMs() + QUESTION_HOLD_SLACK_MS) {
|
|
14761
15427
|
return "waiting";
|
|
14762
15428
|
}
|
|
15429
|
+
if (this.scheduledWakeup.hasPending()) return "waiting";
|
|
14763
15430
|
return this.backgroundWork.hasPending() || isHeavyGateActive() ? "waiting" : "idle";
|
|
14764
15431
|
}
|
|
14765
15432
|
// ── Question grace hold ────────────────────────────────────────────
|
|
@@ -14830,11 +15497,12 @@ ${outcome.failures.join("\n")}
|
|
|
14830
15497
|
this.loopLag.setStatus(loopStatus);
|
|
14831
15498
|
return loopStatus;
|
|
14832
15499
|
}
|
|
14833
|
-
/** One `tool_use` from the turn stream — registers
|
|
14834
|
-
*
|
|
15500
|
+
/** One `tool_use` from the turn stream — registers work that outlives the
|
|
15501
|
+
* turn: a background launch, or an armed `ScheduleWakeup`. */
|
|
14835
15502
|
noteToolUseForBackgroundWork(event) {
|
|
14836
15503
|
if (event.type !== "tool_use" || typeof event.tool !== "string") return;
|
|
14837
15504
|
this.backgroundWork.noteToolUse(event.tool, event.input);
|
|
15505
|
+
this.scheduledWakeup.noteToolUse(event.tool, event.input);
|
|
14838
15506
|
}
|
|
14839
15507
|
async setState(status) {
|
|
14840
15508
|
if (status === "running") {
|
|
@@ -14875,6 +15543,7 @@ ${outcome.failures.join("\n")}
|
|
|
14875
15543
|
/** Mark a fresh turn without emitting a duplicate status. QueryExecutor's
|
|
14876
15544
|
* prefilled PTY callback already emitted `running` before it reaches here. */
|
|
14877
15545
|
beginRunningEpisode() {
|
|
15546
|
+
this.scheduledWakeup.clear();
|
|
14878
15547
|
this.activeEpisodeStartedAt = Date.now();
|
|
14879
15548
|
this.backgroundDeferStartedAt = null;
|
|
14880
15549
|
this.backgroundWakeFiredThisIdle = false;
|
|
@@ -14886,6 +15555,7 @@ ${outcome.failures.join("\n")}
|
|
|
14886
15555
|
this.clearQuestionHold();
|
|
14887
15556
|
this.portDiscovery.stop();
|
|
14888
15557
|
this.backgroundWork.clear();
|
|
15558
|
+
this.scheduledWakeup.clear();
|
|
14889
15559
|
this.loopLag.stop();
|
|
14890
15560
|
this.lifecycle.destroy();
|
|
14891
15561
|
try {
|