@rallycry/conveyor-agent 11.0.42 → 11.0.44
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/{chunk-7RC5YJDF.js → chunk-42YPRICN.js} +12 -3
- package/dist/{chunk-G2CE3NFY.js → chunk-4H75HQVM.js} +1 -1
- package/dist/{chunk-CG7AHC25.js → chunk-KMBMW3J3.js} +156 -31
- package/dist/cli.js +6 -4
- package/dist/index.d.ts +11 -0
- package/dist/index.js +2 -2
- package/dist/{serve-boot-YARBQBSA.js → serve-boot-TZZ45CJR.js} +4 -2
- package/package.json +1 -1
- package/skills/conveyor-build/SKILL.md +20 -5
- package/skills/conveyor-build/references/checkout-claim.md +98 -0
- package/skills/conveyor-build/references/pack-path.md +20 -3
- package/skills/conveyor-build/references/task-path.md +15 -5
- package/skills/conveyor-local-loop/SKILL.md +41 -18
- package/skills/conveyor-release-review/SKILL.md +37 -10
- package/skills/conveyor-release-review/references/dimensions.md +6 -0
- package/skills/conveyor-release-review/references/fan-out.md +130 -0
- package/skills/conveyor-review/SKILL.md +6 -0
- package/skills/conveyor-workflows/SKILL.md +7 -0
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
isCapabilityServableMime,
|
|
83
83
|
isCodexReasoningEffort,
|
|
84
84
|
isPlaceholderLocator,
|
|
85
|
+
loadAndAnnotateDiscoveredPorts,
|
|
85
86
|
locatorMatchesContent,
|
|
86
87
|
parseMentions,
|
|
87
88
|
pushToOrigin,
|
|
@@ -97,7 +98,7 @@ import {
|
|
|
97
98
|
statWorkspacePath,
|
|
98
99
|
updateRemoteToken,
|
|
99
100
|
verifyGitCredential
|
|
100
|
-
} from "./chunk-
|
|
101
|
+
} from "./chunk-KMBMW3J3.js";
|
|
101
102
|
import {
|
|
102
103
|
registerBootMilestoneSocketFallback,
|
|
103
104
|
reportBootMilestone
|
|
@@ -9088,9 +9089,14 @@ var gcpFields = {
|
|
|
9088
9089
|
limit: LIMIT,
|
|
9089
9090
|
pageToken: f.optional(
|
|
9090
9091
|
f.string({ desc: "Opaque token from a previous response to fetch the next page" })
|
|
9092
|
+
),
|
|
9093
|
+
full: f.optional(
|
|
9094
|
+
f.boolean({
|
|
9095
|
+
desc: 'Render each entry in full: message and structured payload untruncated, multi-line. Pair with a narrow search or filter (e.g. insertId="\u2026"); full mode returns at most 10 entries.'
|
|
9096
|
+
})
|
|
9091
9097
|
)
|
|
9092
9098
|
};
|
|
9093
|
-
var GCP_SHARED_DESC = "Envs: 'prod' and 'dev' are the project's Cloud Run apps + Cloud SQL databases (scoped by default to the resources linked in project settings); 'claudespace' is the project's GKE agent-pod namespace. Start broad (severity=ERROR, sinceMinutes=60), then narrow with services/search. Returns compact lines: '<time> <SEVERITY> [<source>] <message> | key=value \u2026' (the key=value tail is the entry's structured payload \u2014 error details, service/method, actor and entity ids). When the response ends with a pageToken line, pass that token back as pageToken for the next page.";
|
|
9099
|
+
var GCP_SHARED_DESC = "Envs: 'prod' and 'dev' are the project's Cloud Run apps + Cloud SQL databases (scoped by default to the resources linked in project settings); 'claudespace' is the project's GKE agent-pod namespace. Start broad (severity=ERROR, sinceMinutes=60), then narrow with services/search. Returns compact lines: '<time> <SEVERITY> [<source>] <message> | key=value \u2026' (the key=value tail is the entry's structured payload \u2014 error details, service/method, actor and entity ids). When the response ends with a pageToken line, pass that token back as pageToken for the next page. To read a whole entry (long message, full payload), re-query narrowly with full=true.";
|
|
9094
9100
|
var queryGcpLogsContract = defineToolContract({
|
|
9095
9101
|
name: "query_gcp_logs",
|
|
9096
9102
|
agent: {
|
|
@@ -12362,6 +12368,7 @@ function handleRateLimitEvent(event, host) {
|
|
|
12362
12368
|
const resetsAt = epochSecondsToISO(rate_limit_info.resetsAt);
|
|
12363
12369
|
const utilization = rate_limit_info.utilization ?? (status === "rejected" ? 1 : void 0);
|
|
12364
12370
|
if (utilization !== void 0 && rate_limit_info.rateLimitType) {
|
|
12371
|
+
const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
|
|
12365
12372
|
host.connection.sendEvent({
|
|
12366
12373
|
type: "rate_limit_update",
|
|
12367
12374
|
rateLimitType: rate_limit_info.rateLimitType,
|
|
@@ -12370,7 +12377,8 @@ function handleRateLimitEvent(event, host) {
|
|
|
12370
12377
|
// The server parses this into the key's sessionResetsAt / weeklyResetsAt;
|
|
12371
12378
|
// Codex reports a reset instant with every gauge, so allowed events carry
|
|
12372
12379
|
// it too, not only rejections.
|
|
12373
|
-
...resetsAt ? { resetsAt } : {}
|
|
12380
|
+
...resetsAt ? { resetsAt } : {},
|
|
12381
|
+
...codingAgentKeyId ? { codingAgentKeyId } : {}
|
|
12374
12382
|
});
|
|
12375
12383
|
}
|
|
12376
12384
|
if (status === "rejected") {
|
|
@@ -15062,6 +15070,7 @@ var SessionRunner = class _SessionRunner {
|
|
|
15062
15070
|
const portVisibility = new CodespacePortVisibility();
|
|
15063
15071
|
this.portDiscovery = deps.portDiscovery ?? new PortDiscovery({
|
|
15064
15072
|
report: (ports) => this.connection.reportDiscoveredPorts(ports),
|
|
15073
|
+
annotate: (ports) => loadAndAnnotateDiscoveredPorts(config.workspaceDir, ports),
|
|
15065
15074
|
onReported: (ports) => portVisibility.ensureVisible(ports.map(({ port }) => port))
|
|
15066
15075
|
});
|
|
15067
15076
|
const initialMode = config.agentMode ?? (config.runnerMode === "pm" ? config.isAuto ? "auto" : "discovery" : "building");
|
|
@@ -1267,12 +1267,20 @@ var AgentConnection = class _AgentConnection {
|
|
|
1267
1267
|
* limited and hand back the best remaining key's credential env (or a
|
|
1268
1268
|
* requeue confirmation when none is left). Awaited: the caller swaps
|
|
1269
1269
|
* credentials and resumes on success, so it needs the real response.
|
|
1270
|
+
*
|
|
1271
|
+
* Names the key this pod is running so the server stamps THAT key: the
|
|
1272
|
+
* session's own attribution can lag the credential a wake handed out, and
|
|
1273
|
+
* stamping it blindly is how one key's cap landed on another. Read from the
|
|
1274
|
+
* env at call time on purpose — `applyCycledKeyEnv` rewrites the var after a
|
|
1275
|
+
* successful cycle, so a second cap names the replacement.
|
|
1270
1276
|
*/
|
|
1271
1277
|
async cycleCodingAgentKey(rateLimitType, resetsAt) {
|
|
1278
|
+
const codingAgentKeyId = process.env.CONVEYOR_CODING_AGENT_KEY_ID;
|
|
1272
1279
|
return await this.call("cycleCodingAgentKey", {
|
|
1273
1280
|
sessionId: this.config.sessionId,
|
|
1274
1281
|
rateLimitType,
|
|
1275
|
-
...resetsAt ? { resetsAt } : {}
|
|
1282
|
+
...resetsAt ? { resetsAt } : {},
|
|
1283
|
+
...codingAgentKeyId ? { codingAgentKeyId } : {}
|
|
1276
1284
|
});
|
|
1277
1285
|
}
|
|
1278
1286
|
// ── Question handling ──────────────────────────────────────────────
|
|
@@ -1530,7 +1538,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
|
|
|
1530
1538
|
}
|
|
1531
1539
|
};
|
|
1532
1540
|
|
|
1533
|
-
// ../shared/dist/chunk-
|
|
1541
|
+
// ../shared/dist/chunk-VSUBG7GR.js
|
|
1534
1542
|
import { z } from "zod";
|
|
1535
1543
|
import { z as z2 } from "zod";
|
|
1536
1544
|
var DEFAULT_SONNET_MODEL = "claude-sonnet-5-5";
|
|
@@ -1684,6 +1692,7 @@ var SEVERITY_ENUM = [
|
|
|
1684
1692
|
];
|
|
1685
1693
|
var MAX_LINE_CHARS = 400;
|
|
1686
1694
|
var DEFAULT_SINCE_MINUTES = 60;
|
|
1695
|
+
var FULL_MODE_MAX_ENTRIES = 10;
|
|
1687
1696
|
function truncateLine(text) {
|
|
1688
1697
|
const oneLine = text.replace(/\s*\n\s*/g, " \u23CE ");
|
|
1689
1698
|
if (oneLine.length <= MAX_LINE_CHARS) return oneLine;
|
|
@@ -1719,7 +1728,8 @@ function formatPayloadSuffix(payloadJson) {
|
|
|
1719
1728
|
try {
|
|
1720
1729
|
parsed = JSON.parse(payloadJson);
|
|
1721
1730
|
} catch {
|
|
1722
|
-
|
|
1731
|
+
const flat = payloadJson.replace(/\s+/g, " ");
|
|
1732
|
+
return ` | payload=${flat.slice(0, PAYLOAD_VALUE_MAX_CHARS)}\u2026[truncated, use full]`;
|
|
1723
1733
|
}
|
|
1724
1734
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
|
|
1725
1735
|
const obj = parsed;
|
|
@@ -1730,14 +1740,50 @@ function formatPayloadSuffix(payloadJson) {
|
|
|
1730
1740
|
const parts = Object.keys(obj).filter((k) => !PAYLOAD_SKIP_KEYS.has(k) && obj[k] !== void 0 && obj[k] !== null).sort((a, b) => rank(a) - rank(b) || a.localeCompare(b)).map((k) => `${k}=${compactPayloadValue(obj[k])}`);
|
|
1731
1741
|
return parts.length > 0 ? ` | ${parts.join(" ")}` : "";
|
|
1732
1742
|
}
|
|
1743
|
+
function httpPrefixOf(entry) {
|
|
1744
|
+
return entry.httpRequest?.status ? `http ${entry.httpRequest.status} ${entry.httpRequest.method ?? ""} ${entry.httpRequest.url ?? ""}`.trim() + " \u2014 " : "";
|
|
1745
|
+
}
|
|
1733
1746
|
function formatLogEntryLine(entry) {
|
|
1734
|
-
const httpPrefix = entry
|
|
1747
|
+
const httpPrefix = httpPrefixOf(entry);
|
|
1735
1748
|
return `${entry.timestamp} ${entry.severity.padEnd(7)} [${entrySource(entry)}] ${truncateLine(
|
|
1736
1749
|
`${httpPrefix}${entry.message}${formatPayloadSuffix(entry.payload)}`
|
|
1737
1750
|
)}`;
|
|
1738
1751
|
}
|
|
1752
|
+
function formatPayloadFull(payloadJson) {
|
|
1753
|
+
let parsed;
|
|
1754
|
+
try {
|
|
1755
|
+
parsed = JSON.parse(payloadJson);
|
|
1756
|
+
} catch {
|
|
1757
|
+
return payloadJson;
|
|
1758
|
+
}
|
|
1759
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1760
|
+
return JSON.stringify(parsed, null, 2);
|
|
1761
|
+
}
|
|
1762
|
+
const obj = Object.fromEntries(
|
|
1763
|
+
Object.entries(parsed).filter(([k]) => !PAYLOAD_SKIP_KEYS.has(k))
|
|
1764
|
+
);
|
|
1765
|
+
return Object.keys(obj).length > 0 ? JSON.stringify(obj, null, 2) : "";
|
|
1766
|
+
}
|
|
1767
|
+
function formatLogEntryFull(entry) {
|
|
1768
|
+
const head = `${entry.timestamp} ${entry.severity} [${entrySource(entry)}]${entry.insertId ? ` insertId=${entry.insertId}` : ""}`;
|
|
1769
|
+
const payload = entry.payload ? formatPayloadFull(entry.payload) : "";
|
|
1770
|
+
return [
|
|
1771
|
+
head,
|
|
1772
|
+
`${httpPrefixOf(entry)}${entry.message}`,
|
|
1773
|
+
...payload ? [payload] : [],
|
|
1774
|
+
...entry.payloadTruncated ? ["\u2026[payload truncated server-side]"] : []
|
|
1775
|
+
].join("\n");
|
|
1776
|
+
}
|
|
1777
|
+
function fullModeLimit(limit) {
|
|
1778
|
+
return Math.min(limit ?? FULL_MODE_MAX_ENTRIES, FULL_MODE_MAX_ENTRIES);
|
|
1779
|
+
}
|
|
1780
|
+
function formatFullEntries(entries) {
|
|
1781
|
+
return entries.map((entry, i) => (i === 0 ? "" : "\n") + formatLogEntryFull(entry));
|
|
1782
|
+
}
|
|
1739
1783
|
async function runQueryGcpLogs(port, params, now = Date.now) {
|
|
1740
1784
|
const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
|
|
1785
|
+
const full = params.full === true;
|
|
1786
|
+
const limit = full ? fullModeLimit(params.limit) : params.limit;
|
|
1741
1787
|
const result = await port.queryGcpLogs({
|
|
1742
1788
|
projectId: params.projectId,
|
|
1743
1789
|
env: params.env,
|
|
@@ -1749,8 +1795,9 @@ async function runQueryGcpLogs(port, params, now = Date.now) {
|
|
|
1749
1795
|
filter: params.filter,
|
|
1750
1796
|
startTime,
|
|
1751
1797
|
endTime: params.endTime,
|
|
1752
|
-
limit
|
|
1753
|
-
pageToken: params.pageToken
|
|
1798
|
+
limit,
|
|
1799
|
+
pageToken: params.pageToken,
|
|
1800
|
+
...full ? { full: true } : {}
|
|
1754
1801
|
});
|
|
1755
1802
|
if (result.error) return result.error;
|
|
1756
1803
|
const header = [
|
|
@@ -1758,9 +1805,10 @@ async function runQueryGcpLogs(port, params, now = Date.now) {
|
|
|
1758
1805
|
`window=${startTime}\u2192${params.endTime ?? "now"}`,
|
|
1759
1806
|
...params.severity ? [`minSeverity=${params.severity}`] : [],
|
|
1760
1807
|
`scope=${result.scopedServices ? `[${result.scopedServices.join(", ")}]` : "all"}`,
|
|
1761
|
-
`entries=${result.entries.length}
|
|
1808
|
+
`entries=${result.entries.length}`,
|
|
1809
|
+
...full ? ["full=true"] : []
|
|
1762
1810
|
].join(" ");
|
|
1763
|
-
const lines = result.entries.map(formatLogEntryLine);
|
|
1811
|
+
const lines = full ? formatFullEntries(result.entries) : result.entries.map(formatLogEntryLine);
|
|
1764
1812
|
const footer = result.nextPageToken ? [`-- more available: pass pageToken="${result.nextPageToken}" to continue`] : [];
|
|
1765
1813
|
if (lines.length === 0) {
|
|
1766
1814
|
return [
|
|
@@ -2364,7 +2412,8 @@ var DiscoveredPortSchema = z4.object({
|
|
|
2364
2412
|
port: z4.number().int().min(1).max(65535),
|
|
2365
2413
|
label: z4.string().min(1).max(64).optional(),
|
|
2366
2414
|
protocol: z4.enum(["http", "tcp"]).optional(),
|
|
2367
|
-
detectedAt: z4.string()
|
|
2415
|
+
detectedAt: z4.string(),
|
|
2416
|
+
hidden: z4.boolean().optional()
|
|
2368
2417
|
});
|
|
2369
2418
|
var ReportDiscoveredPortsRequestSchema = z4.object({
|
|
2370
2419
|
sessionId: z4.string(),
|
|
@@ -2511,7 +2560,11 @@ var SubmitCodeReviewResultRequestSchema = z4.object({
|
|
|
2511
2560
|
var CycleCodingAgentKeyRequestSchema = z4.object({
|
|
2512
2561
|
sessionId: z4.string(),
|
|
2513
2562
|
rateLimitType: z4.string(),
|
|
2514
|
-
resetsAt: z4.string().optional()
|
|
2563
|
+
resetsAt: z4.string().optional(),
|
|
2564
|
+
// The key the pod is actually running (CONVEYOR_CODING_AGENT_KEY_ID). Wins
|
|
2565
|
+
// over the session's attribution stamp after owner validation; optional so
|
|
2566
|
+
// older agents keep the session fallback.
|
|
2567
|
+
codingAgentKeyId: z4.string().optional()
|
|
2515
2568
|
});
|
|
2516
2569
|
var PostChildChatMessageRequestSchema = z4.object({
|
|
2517
2570
|
sessionId: z4.string(),
|
|
@@ -2925,11 +2978,12 @@ var QueryProjectGcpLogsRequestSchema = z6.object({
|
|
|
2925
2978
|
sqlInstances: z6.array(z6.string().min(1).max(200)).max(25).optional(),
|
|
2926
2979
|
allServices: z6.boolean().optional(),
|
|
2927
2980
|
search: z6.string().max(256).optional(),
|
|
2928
|
-
filter: z6.string().max(
|
|
2981
|
+
filter: z6.string().max(2e3).optional(),
|
|
2929
2982
|
startTime: z6.string().optional(),
|
|
2930
2983
|
endTime: z6.string().optional(),
|
|
2931
2984
|
limit: z6.number().int().min(1).max(200).optional().default(50),
|
|
2932
|
-
pageToken: z6.string().max(4096).optional()
|
|
2985
|
+
pageToken: z6.string().max(4096).optional(),
|
|
2986
|
+
full: z6.boolean().optional()
|
|
2933
2987
|
});
|
|
2934
2988
|
var QueryProjectGrafanaLogsRequestSchema = z6.object({
|
|
2935
2989
|
projectId: z6.string(),
|
|
@@ -4831,6 +4885,20 @@ async function workspacePathExists(path) {
|
|
|
4831
4885
|
// src/setup/config.ts
|
|
4832
4886
|
var DEVCONTAINER_PATH = ".devcontainer/conveyor/devcontainer.json";
|
|
4833
4887
|
var DEVCONTAINER_PORT_DENY_LIST = /* @__PURE__ */ new Set([5432, 6379, 9200]);
|
|
4888
|
+
var MAX_DISCOVERED_LABEL_LENGTH = 64;
|
|
4889
|
+
var EXACT_PORT_KEY = /^\d+$/;
|
|
4890
|
+
var RANGE_PORT_KEY = /^(\d+)-(\d+)$/;
|
|
4891
|
+
function parsePortAttributes(value) {
|
|
4892
|
+
if (!value || typeof value !== "object") return null;
|
|
4893
|
+
const raw = value;
|
|
4894
|
+
const entry = {};
|
|
4895
|
+
if (typeof raw.label === "string") entry.label = raw.label;
|
|
4896
|
+
if (raw.visibility === "public" || raw.visibility === "private") {
|
|
4897
|
+
entry.visibility = raw.visibility;
|
|
4898
|
+
}
|
|
4899
|
+
if (typeof raw.onAutoForward === "string") entry.hidden = raw.onAutoForward === "ignore";
|
|
4900
|
+
return entry;
|
|
4901
|
+
}
|
|
4834
4902
|
async function loadForwardPorts(workspaceDir) {
|
|
4835
4903
|
try {
|
|
4836
4904
|
const raw = await readWorkspaceFile(join(workspaceDir, DEVCONTAINER_PATH));
|
|
@@ -4840,37 +4908,84 @@ async function loadForwardPorts(workspaceDir) {
|
|
|
4840
4908
|
);
|
|
4841
4909
|
const attributes = {};
|
|
4842
4910
|
for (const [key, value] of Object.entries(parsed.portsAttributes ?? {})) {
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
if (typeof value.label === "string") entry.label = value.label;
|
|
4846
|
-
if (value.visibility === "public" || value.visibility === "private") {
|
|
4847
|
-
entry.visibility = value.visibility;
|
|
4848
|
-
}
|
|
4849
|
-
if (value.onAutoForward === "ignore") entry.hidden = true;
|
|
4850
|
-
attributes[key] = entry;
|
|
4911
|
+
const entry = parsePortAttributes(value);
|
|
4912
|
+
if (entry) attributes[key] = entry;
|
|
4851
4913
|
}
|
|
4914
|
+
const otherAttributes = parsePortAttributes(parsed.otherPortsAttributes);
|
|
4852
4915
|
const links = sanitizeSessionPreviewLinks(parsed.customizations?.conveyor?.previewLinks);
|
|
4853
|
-
return { ports, attributes, links };
|
|
4916
|
+
return { ports, attributes, ...otherAttributes ? { otherAttributes } : {}, links };
|
|
4854
4917
|
} catch {
|
|
4855
4918
|
return { ports: [], attributes: {}, links: [] };
|
|
4856
4919
|
}
|
|
4857
4920
|
}
|
|
4921
|
+
function matchingRanges(attributes, port) {
|
|
4922
|
+
const matches = [];
|
|
4923
|
+
for (const [key, attr] of Object.entries(attributes)) {
|
|
4924
|
+
const range = RANGE_PORT_KEY.exec(key);
|
|
4925
|
+
if (!range) continue;
|
|
4926
|
+
const start = Number(range[1]);
|
|
4927
|
+
const end = Number(range[2]);
|
|
4928
|
+
if (start <= port && port <= end) matches.push({ start, attr });
|
|
4929
|
+
}
|
|
4930
|
+
return matches.sort((a, b) => a.start - b.start).map(({ attr }) => attr);
|
|
4931
|
+
}
|
|
4932
|
+
function exactAttributes(attributes, port) {
|
|
4933
|
+
const match = Object.entries(attributes).find(
|
|
4934
|
+
([key]) => EXACT_PORT_KEY.test(key) && Number(key) === port
|
|
4935
|
+
);
|
|
4936
|
+
return match?.[1];
|
|
4937
|
+
}
|
|
4938
|
+
function resolvePortAttributes(result, port) {
|
|
4939
|
+
const exact = exactAttributes(result.attributes, port);
|
|
4940
|
+
const ranges = matchingRanges(result.attributes, port);
|
|
4941
|
+
const layers = [];
|
|
4942
|
+
if (exact) layers.push(["exact", exact]);
|
|
4943
|
+
for (const range of ranges) layers.push(["range", range]);
|
|
4944
|
+
const declared = layers.length > 0 || result.ports.includes(port);
|
|
4945
|
+
if (!declared && result.otherAttributes) layers.push(["other", result.otherAttributes]);
|
|
4946
|
+
const attrs = {};
|
|
4947
|
+
let labelSource = null;
|
|
4948
|
+
for (const [source, layer] of layers) {
|
|
4949
|
+
if (attrs.label === void 0 && layer.label !== void 0) {
|
|
4950
|
+
attrs.label = layer.label;
|
|
4951
|
+
labelSource = source;
|
|
4952
|
+
}
|
|
4953
|
+
attrs.visibility ??= layer.visibility;
|
|
4954
|
+
attrs.hidden ??= layer.hidden;
|
|
4955
|
+
}
|
|
4956
|
+
if (attrs.visibility === void 0) delete attrs.visibility;
|
|
4957
|
+
if (attrs.hidden === void 0) delete attrs.hidden;
|
|
4958
|
+
return { attrs, labelSource };
|
|
4959
|
+
}
|
|
4858
4960
|
function buildSessionPreviewPorts(result) {
|
|
4859
4961
|
const toEntry = (port) => {
|
|
4860
|
-
const
|
|
4962
|
+
const { attrs } = resolvePortAttributes(result, port);
|
|
4861
4963
|
const entry = { port };
|
|
4862
|
-
if (
|
|
4863
|
-
if (
|
|
4864
|
-
if (
|
|
4964
|
+
if (attrs.label) entry.label = attrs.label;
|
|
4965
|
+
if (attrs.visibility) entry.visibility = attrs.visibility;
|
|
4966
|
+
if (attrs.hidden) entry.hidden = true;
|
|
4865
4967
|
return entry;
|
|
4866
4968
|
};
|
|
4867
4969
|
const forwarded = result.ports.filter((port) => !DEVCONTAINER_PORT_DENY_LIST.has(port));
|
|
4868
4970
|
const listed = new Set(forwarded);
|
|
4869
|
-
const hiddenOnly = Object.entries(result.attributes).filter(([, attr]) => attr.hidden).map(([key]) => Number(key)).filter(
|
|
4971
|
+
const hiddenOnly = Object.entries(result.attributes).filter(([key, attr]) => attr.hidden && EXACT_PORT_KEY.test(key)).map(([key]) => Number(key)).filter(
|
|
4870
4972
|
(port) => Number.isInteger(port) && port >= 1 && port <= 65535 && !listed.has(port) && !DEVCONTAINER_PORT_DENY_LIST.has(port)
|
|
4871
4973
|
);
|
|
4872
4974
|
return [...forwarded, ...hiddenOnly].map(toEntry);
|
|
4873
4975
|
}
|
|
4976
|
+
function annotateDiscoveredPorts(result, ports) {
|
|
4977
|
+
return ports.map((entry) => {
|
|
4978
|
+
const { attrs, labelSource } = resolvePortAttributes(result, entry.port);
|
|
4979
|
+
const annotated = { ...entry };
|
|
4980
|
+
if (attrs.hidden) annotated.hidden = true;
|
|
4981
|
+
const label = attrs.label?.trim().slice(0, MAX_DISCOVERED_LABEL_LENGTH);
|
|
4982
|
+
if (label && (labelSource === "range" || labelSource === "other")) annotated.label = label;
|
|
4983
|
+
return annotated;
|
|
4984
|
+
});
|
|
4985
|
+
}
|
|
4986
|
+
async function loadAndAnnotateDiscoveredPorts(workspaceDir, ports) {
|
|
4987
|
+
return annotateDiscoveredPorts(await loadForwardPorts(workspaceDir), ports);
|
|
4988
|
+
}
|
|
4874
4989
|
function loadConveyorConfig() {
|
|
4875
4990
|
const envStart = process.env.CONVEYOR_START_COMMAND;
|
|
4876
4991
|
if (envStart) {
|
|
@@ -5329,9 +5444,18 @@ var PortDiscovery = class {
|
|
|
5329
5444
|
confirmedKey() {
|
|
5330
5445
|
return this.confirmedPorts().map(({ port }) => port).join(",");
|
|
5331
5446
|
}
|
|
5447
|
+
async annotateSafe(ports) {
|
|
5448
|
+
if (!this.opts.annotate) return ports;
|
|
5449
|
+
try {
|
|
5450
|
+
return await this.opts.annotate(ports);
|
|
5451
|
+
} catch {
|
|
5452
|
+
return ports;
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5332
5455
|
async flushReport() {
|
|
5333
|
-
const
|
|
5334
|
-
const key =
|
|
5456
|
+
const confirmed = this.confirmedPorts();
|
|
5457
|
+
const key = confirmed.map(({ port }) => port).join(",");
|
|
5458
|
+
const ports = await this.annotateSafe(confirmed);
|
|
5335
5459
|
try {
|
|
5336
5460
|
await this.opts.report(ports);
|
|
5337
5461
|
this.lastReportedKey = key;
|
|
@@ -5481,9 +5605,10 @@ export {
|
|
|
5481
5605
|
pushToOrigin,
|
|
5482
5606
|
flushAllPendingWork,
|
|
5483
5607
|
awaitGitReady,
|
|
5484
|
-
PortDiscovery,
|
|
5485
|
-
CodespacePortVisibility,
|
|
5486
5608
|
loadForwardPorts,
|
|
5487
5609
|
buildSessionPreviewPorts,
|
|
5488
|
-
|
|
5610
|
+
loadAndAnnotateDiscoveredPorts,
|
|
5611
|
+
loadConveyorConfig,
|
|
5612
|
+
PortDiscovery,
|
|
5613
|
+
CodespacePortVisibility
|
|
5489
5614
|
};
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
WorkspaceCommandSupervisor,
|
|
8
8
|
startWorkspaceCommandsAfterConnect,
|
|
9
9
|
stopWorkspaceCommands
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-4H75HQVM.js";
|
|
11
11
|
import {
|
|
12
12
|
PtyHarness,
|
|
13
13
|
SessionRunner,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
runUsageProbe,
|
|
28
28
|
sampleKeyUsage,
|
|
29
29
|
usesNativeUsageReporting
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-42YPRICN.js";
|
|
31
31
|
import "./chunk-37J5MMQT.js";
|
|
32
32
|
import {
|
|
33
33
|
inheritedEnv,
|
|
@@ -45,8 +45,9 @@ import {
|
|
|
45
45
|
applyBootstrapToEnv,
|
|
46
46
|
createServiceLogger,
|
|
47
47
|
fetchBootstrap,
|
|
48
|
+
loadAndAnnotateDiscoveredPorts,
|
|
48
49
|
loadConveyorConfig
|
|
49
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-KMBMW3J3.js";
|
|
50
51
|
import "./chunk-Q4FQOJ7D.js";
|
|
51
52
|
import "./chunk-W4LZ7R6Z.js";
|
|
52
53
|
import "./chunk-372R6E4C.js";
|
|
@@ -417,6 +418,7 @@ var AdhocSessionRunner = class {
|
|
|
417
418
|
const portVisibility = new CodespacePortVisibility();
|
|
418
419
|
this.portDiscovery = deps?.portDiscovery ?? new PortDiscovery({
|
|
419
420
|
report: (ports) => this.connection.reportDiscoveredPorts(ports),
|
|
421
|
+
annotate: (ports) => loadAndAnnotateDiscoveredPorts(config.workspaceDir, ports),
|
|
420
422
|
onReported: (ports) => portVisibility.ensureVisible(ports.map(({ port }) => port))
|
|
421
423
|
});
|
|
422
424
|
this.commandSupervisor = deps?.commandSupervisor ?? new WorkspaceCommandSupervisor({
|
|
@@ -1468,7 +1470,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
|
|
|
1468
1470
|
process.exit(1);
|
|
1469
1471
|
}
|
|
1470
1472
|
if (CONVEYOR_MODE === "serving") {
|
|
1471
|
-
const { runServingSession } = await import("./serve-boot-
|
|
1473
|
+
const { runServingSession } = await import("./serve-boot-TZZ45CJR.js");
|
|
1472
1474
|
exitContext.runnerMode = "serving";
|
|
1473
1475
|
exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
|
|
1474
1476
|
const outcome = await runServingSession({
|
package/dist/index.d.ts
CHANGED
|
@@ -374,6 +374,12 @@ declare class AgentConnection {
|
|
|
374
374
|
* limited and hand back the best remaining key's credential env (or a
|
|
375
375
|
* requeue confirmation when none is left). Awaited: the caller swaps
|
|
376
376
|
* credentials and resumes on success, so it needs the real response.
|
|
377
|
+
*
|
|
378
|
+
* Names the key this pod is running so the server stamps THAT key: the
|
|
379
|
+
* session's own attribution can lag the credential a wake handed out, and
|
|
380
|
+
* stamping it blindly is how one key's cap landed on another. Read from the
|
|
381
|
+
* env at call time on purpose — `applyCycledKeyEnv` rewrites the var after a
|
|
382
|
+
* successful cycle, so a second cap names the replacement.
|
|
377
383
|
*/
|
|
378
384
|
cycleCodingAgentKey(rateLimitType: string, resetsAt?: string): Promise<{
|
|
379
385
|
cycled: true;
|
|
@@ -1315,6 +1321,9 @@ declare function removeWorktree(projectDir: string, taskId: string): Promise<voi
|
|
|
1315
1321
|
interface ConveyorConfig {
|
|
1316
1322
|
startCommand?: string;
|
|
1317
1323
|
}
|
|
1324
|
+
/** `hidden` is tri-state: `true` for `onAutoForward: "ignore"`, `false` for
|
|
1325
|
+
* any other `onAutoForward` value (so an exact `"notify"` can override a
|
|
1326
|
+
* range `"ignore"`), absent when the entry does not set it. */
|
|
1318
1327
|
type PortAttributes = {
|
|
1319
1328
|
label?: string;
|
|
1320
1329
|
visibility?: "public" | "private";
|
|
@@ -1323,6 +1332,8 @@ type PortAttributes = {
|
|
|
1323
1332
|
interface ForwardPortsResult {
|
|
1324
1333
|
ports: number[];
|
|
1325
1334
|
attributes: Record<string, PortAttributes>;
|
|
1335
|
+
/** `otherPortsAttributes` — applies only to ports nothing else declares. */
|
|
1336
|
+
otherAttributes?: PortAttributes;
|
|
1326
1337
|
/** `customizations.conveyor.previewLinks`, sanitized. */
|
|
1327
1338
|
links: SessionPreviewLink[];
|
|
1328
1339
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SessionRunner,
|
|
3
3
|
unshallowRepo
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-42YPRICN.js";
|
|
5
5
|
import "./chunk-37J5MMQT.js";
|
|
6
6
|
import "./chunk-EKTPDG3G.js";
|
|
7
7
|
import {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
stageAndCommit,
|
|
18
18
|
updateRemoteToken,
|
|
19
19
|
workspacePathExists
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-KMBMW3J3.js";
|
|
21
21
|
import "./chunk-Q4FQOJ7D.js";
|
|
22
22
|
import {
|
|
23
23
|
runAuthTokenCommand,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
WorkspaceCommandSupervisor
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-4H75HQVM.js";
|
|
4
4
|
import {
|
|
5
5
|
AgentConnection,
|
|
6
6
|
CodespacePortVisibility,
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
awaitGitReady,
|
|
11
11
|
createServiceLogger,
|
|
12
12
|
ensureOnTaskBranch,
|
|
13
|
+
loadAndAnnotateDiscoveredPorts,
|
|
13
14
|
loadConveyorConfig
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-KMBMW3J3.js";
|
|
15
16
|
import "./chunk-Q4FQOJ7D.js";
|
|
16
17
|
import "./chunk-W4LZ7R6Z.js";
|
|
17
18
|
import "./chunk-IA45XHOA.js";
|
|
@@ -38,6 +39,7 @@ var ServeSessionRunner = class {
|
|
|
38
39
|
const portVisibility = new CodespacePortVisibility();
|
|
39
40
|
this.portDiscovery = deps?.portDiscovery ?? new PortDiscovery({
|
|
40
41
|
report: (ports) => this.connection.reportDiscoveredPorts(ports),
|
|
42
|
+
annotate: (ports) => loadAndAnnotateDiscoveredPorts(config.workspaceDir, ports),
|
|
41
43
|
onReported: (ports) => portVisibility.ensureVisible(ports.map(({ port }) => port))
|
|
42
44
|
});
|
|
43
45
|
this.commandSupervisor = deps?.commandSupervisor ?? new WorkspaceCommandSupervisor({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rallycry/conveyor-agent",
|
|
3
|
-
"version": "11.0.
|
|
3
|
+
"version": "11.0.44",
|
|
4
4
|
"description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -102,6 +102,12 @@ task binding and no user account.
|
|
|
102
102
|
stack) is the entire value of local execution, and worktrees have none of it.
|
|
103
103
|
A dirty `git status` before any branch switch is a hard stop: report and let
|
|
104
104
|
the user commit or stash. Do not solve it with a second checkout.
|
|
105
|
+
- **Take turns on the checkout.** Another local session can share this
|
|
106
|
+
checkout, and a clean tree does not prove it is idle. Hold the checkout
|
|
107
|
+
claim from before the card claim until the finish line, and never switch
|
|
108
|
+
branches while another session holds it — the protocol, commands, and what
|
|
109
|
+
each result means are in
|
|
110
|
+
[references/checkout-claim.md](references/checkout-claim.md). A pod skips it.
|
|
105
111
|
- The dev database and dev-server ports are shared with the user's own
|
|
106
112
|
sessions. No destructive experiments, never reset the dev DB, and reuse a
|
|
107
113
|
running dev stack rather than fighting over ports.
|
|
@@ -149,10 +155,13 @@ say why.
|
|
|
149
155
|
|
|
150
156
|
## Claim
|
|
151
157
|
|
|
152
|
-
Re-confirm the card is still claimable
|
|
158
|
+
Re-confirm the card is still claimable. **Locally**, take the checkout claim
|
|
159
|
+
first (`checkout acquire --card <slug>`,
|
|
160
|
+
[references/checkout-claim.md](references/checkout-claim.md)); `held` means
|
|
161
|
+
flag and wait, not claim. Then `mcp__conveyor__update_task` →
|
|
153
162
|
`status: "InProgress"`, then `mcp__conveyor__post_to_chat` with a claim marker
|
|
154
163
|
naming where you are running. Status changed under you → someone else took it;
|
|
155
|
-
stop.
|
|
164
|
+
release the checkout claim and stop.
|
|
156
165
|
|
|
157
166
|
**A pack parent is claimed differently** — see the pack path. The parent stays
|
|
158
167
|
parked and the chat marker IS the claim.
|
|
@@ -285,7 +294,10 @@ Refresh against the card's base first, then gate, then open — in that order, s
|
|
|
285
294
|
nothing invalidates the verification you just did:
|
|
286
295
|
|
|
287
296
|
1. `git fetch origin <base> && git merge origin/<base> --no-edit`
|
|
288
|
-
2. One verification pass, scoped to the diff.
|
|
297
|
+
2. One verification pass, scoped to the diff. **Locally**, run
|
|
298
|
+
`checkout verify --card <slug>` before the pass, each commit and push, and
|
|
299
|
+
the PR call; anything but `ok` means stop (see
|
|
300
|
+
[references/checkout-claim.md](references/checkout-claim.md)).
|
|
289
301
|
3. `mcp__conveyor__create_pull_request`, naming the base branch **explicitly**.
|
|
290
302
|
**The two surfaces spell these differently and unknown keys are silently
|
|
291
303
|
dropped, not rejected** — locally it is `head:` / `base:`; in a pod it is
|
|
@@ -298,6 +310,8 @@ nothing invalidates the verification you just did:
|
|
|
298
310
|
should look at, and anything you did NOT do.
|
|
299
311
|
|
|
300
312
|
Then confirm CI actually started (read-only `gh pr checks`). Do not wait on it.
|
|
313
|
+
**Locally**, that is the finish line for the checkout too:
|
|
314
|
+
`checkout release --card <slug>`.
|
|
301
315
|
|
|
302
316
|
**Do not re-merge the base and do not re-run a gate that already passed.** If
|
|
303
317
|
the base moved while the gates ran, open the PR anyway — CI validates against
|
|
@@ -332,8 +346,9 @@ After two genuinely different failed approaches, or on a decision only the user
|
|
|
332
346
|
can make: post the reason AND the specific question to chat, set the card back
|
|
333
347
|
to `"Open"`, restore the tree (locally, `git checkout dev` — leaving a shared
|
|
334
348
|
checkout parked on an abandoned feature branch is how the next session starts
|
|
335
|
-
from the wrong base),
|
|
336
|
-
|
|
349
|
+
from the wrong base), release the checkout claim locally
|
|
350
|
+
(`checkout release --card <slug>`), and stop. A vague "this is hard" is not a
|
|
351
|
+
handoff; the question is what makes it one.
|
|
337
352
|
|
|
338
353
|
**A thin plan is a different case, and it does not stop a pack.** A card whose
|
|
339
354
|
plan fails the context-free-reader bar: post what is missing to its chat and
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Checkout claim — local sessions take turns on one checkout
|
|
2
|
+
|
|
3
|
+
The protocol behind every "hold the checkout claim" line in
|
|
4
|
+
[conveyor-build](../SKILL.md), its [task path](task-path.md) and
|
|
5
|
+
[pack path](pack-path.md), [conveyor-local-loop](../../conveyor-local-loop/SKILL.md),
|
|
6
|
+
and local fixes in [conveyor-review](../../conveyor-review/SKILL.md).
|
|
7
|
+
|
|
8
|
+
**Why.** The card claim (`InProgress` plus a chat marker) stops two sessions
|
|
9
|
+
working ONE card. It does nothing about two sessions working two cards in ONE
|
|
10
|
+
checkout. A clean tree passes the "dirty `git status` → stop" rule even while
|
|
11
|
+
another session is mid-task, so `git checkout -B <branch>` succeeds and switches
|
|
12
|
+
the branch underneath it. The checkout claim closes that gap: one session holds
|
|
13
|
+
the working tree at a time, and every other session waits.
|
|
14
|
+
|
|
15
|
+
**Scope: local only.** A pod has its own checkout and never runs this. Skip
|
|
16
|
+
every checkout-claim step when you are in a pod.
|
|
17
|
+
|
|
18
|
+
## How to call it
|
|
19
|
+
|
|
20
|
+
The helper is the `checkout` subcommand of the `conveyor-skills` bin. Pick the
|
|
21
|
+
form once per session:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
[ -x node_modules/.bin/conveyor-skills ] && echo local || echo npx
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `local` → `node_modules/.bin/conveyor-skills checkout <command> …`
|
|
28
|
+
- `npx` → `npx -y -p @rallycry/conveyor-skills@latest conveyor-skills checkout <command> …`
|
|
29
|
+
- The local bin answers `unknown command 'checkout'` (an older branch or an old
|
|
30
|
+
install) → use the `npx` form.
|
|
31
|
+
|
|
32
|
+
Write the chosen form out literally in every call. Never store it in a shell
|
|
33
|
+
variable and run `$VAR checkout …`: zsh does not word-split it.
|
|
34
|
+
|
|
35
|
+
`--card` is the card you are working. Inside a pack it is ALWAYS the pack
|
|
36
|
+
parent's slug, for the whole run. Identity comes from `CONVEYOR_SESSION_ID`, then
|
|
37
|
+
`CLAUDE_CODE_SESSION_ID`. A runtime that sets neither passes a stable
|
|
38
|
+
`--session <id>` on every call; without it the identity falls back to
|
|
39
|
+
`card:<slug>`, which still works but frees a dead holder only after the TTL.
|
|
40
|
+
|
|
41
|
+
The claim is one file, `<git dir>/conveyor-checkout-claim.json`. It is never
|
|
42
|
+
committed and never shows in `git status`. Each command prints ONE line of
|
|
43
|
+
JSON on stdout (`{"command","state","head","dirty",…}`) and a hint on stderr.
|
|
44
|
+
|
|
45
|
+
## When
|
|
46
|
+
|
|
47
|
+
| Moment | Command |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Before the first git write or card status write (before the claiming `update_task`) | `acquire --card <slug>` |
|
|
50
|
+
| After every branch create or switch | `acquire --card <slug> --branch <branch>` |
|
|
51
|
+
| Before each gate, commit, push, and `create_pull_request`; at the start of every wake while you hold the claim | `verify --card <slug>` (also refreshes the heartbeat) |
|
|
52
|
+
| Finish line reached (PR open, CI started), parked, blocked, or the card claim lost — after restoring the tree | `release --card <slug>` |
|
|
53
|
+
|
|
54
|
+
## What each result means
|
|
55
|
+
|
|
56
|
+
| Command | State (exit) | Do this |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `acquire` | `acquired`, `refreshed` (0) | Carry on. |
|
|
59
|
+
| `acquire` | `took-over` (0) | The previous holder was dead or stale. If `dirty` is true, the tree holds that claim's unfinished work for THIS card: audit it before you continue. |
|
|
60
|
+
| `acquire` | `held` (3) | Another session holds the checkout. Touch nothing — no checkout, no status write, no commit. Flag it (below), then wait. |
|
|
61
|
+
| `acquire` | `dirty` (5) | The tree is dirty and no live claim covers it. Hard stop: report it. The user commits or stashes, never you. |
|
|
62
|
+
| `verify` | `ok` (0) | Carry on. |
|
|
63
|
+
| `verify` | `branch-moved` (4) | HEAD is not the claimed branch — someone switched it, or your own checkout failed and left you on the previous branch. Do not commit or push. Report it and stop. |
|
|
64
|
+
| `verify` | `lost` (3) | You no longer hold the claim. Stop touching the tree and `acquire` again, then follow that result. |
|
|
65
|
+
| `release` | `released`, `none` (0) | Done. |
|
|
66
|
+
| `release` | `held` (3) | The claim is not yours. Leave it. |
|
|
67
|
+
| any | `error` (1) | Usage error or not a git repo. Read the stderr line. |
|
|
68
|
+
|
|
69
|
+
**Flag, then wait.** On `held`, tell the user which card, branch and host hold
|
|
70
|
+
the checkout (the `holder` field has all three). A standalone build also posts
|
|
71
|
+
one line to its card's chat, for example
|
|
72
|
+
`[build] waiting — checkout held by <card> on <branch>`. Then wait:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
node_modules/.bin/conveyor-skills checkout wait --timeout 1740 # or the npx form
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
In Claude Code, launch it with Bash `run_in_background: true` and end the
|
|
79
|
+
turn. Its completion notification is the wake. In Codex, hold the command
|
|
80
|
+
session open until it returns. The result is advisory, like `conveyor-wait`:
|
|
81
|
+
`free`, `stale`, or `own` → run `acquire` again; `timeout` → arm ONE new wait.
|
|
82
|
+
Never arm a second wait while one is running.
|
|
83
|
+
|
|
84
|
+
## Rules that go with the claim
|
|
85
|
+
|
|
86
|
+
- **Stage by path, never `git add -A`.** In a shared checkout an untracked file
|
|
87
|
+
can belong to another session.
|
|
88
|
+
- **No claim, dirty tree → refuse.** `acquire` enforces the old "dirty tree is
|
|
89
|
+
a hard stop" rule deterministically. It also covers ad-hoc sessions that
|
|
90
|
+
never take a claim.
|
|
91
|
+
- **A stale claim is taken over only on a clean tree, or when it names the
|
|
92
|
+
same card.** A holder is stale when its pid is dead on this host, when the
|
|
93
|
+
same pid now runs a different session (a `/clear`), or when its heartbeat is
|
|
94
|
+
older than the TTL (7200s; `CONVEYOR_CHECKOUT_CLAIM_TTL_SECONDS` or `--ttl`
|
|
95
|
+
overrides it).
|
|
96
|
+
- **Human override.** `checkout status` shows the holder and never changes
|
|
97
|
+
anything. `checkout release --force` drops someone else's claim, or a claim
|
|
98
|
+
file that does not parse. Run it only when the user tells you to.
|
|
@@ -24,6 +24,13 @@ alternative is where pack incidents come from.
|
|
|
24
24
|
IMMEDIATELY record it: `mcp__conveyor__update_task` with
|
|
25
25
|
`githubBranch: <branch>`.
|
|
26
26
|
|
|
27
|
+
**Locally, take the checkout claim before any of this** —
|
|
28
|
+
`checkout acquire --card <parent-slug>` before you cut or check out the
|
|
29
|
+
pack branch, then `acquire --card <parent-slug> --branch <pack>` once you
|
|
30
|
+
are on it ([checkout-claim.md](checkout-claim.md)). The pack holds the
|
|
31
|
+
claim for the WHOLE run: `--card` stays the parent's slug through every
|
|
32
|
+
child, and `--branch` follows each child-branch and pack-branch switch.
|
|
33
|
+
|
|
27
34
|
That write is load-bearing, not bookkeeping. Identification mints a
|
|
28
35
|
competing `conveyor/*` branch name onto any branchless card it processes,
|
|
29
36
|
and every pack-child merge handler keys on the card's recorded branch
|
|
@@ -121,6 +128,10 @@ state and is a no-op. This is NOT the "insurance wakeup" the pod prompt
|
|
|
121
128
|
forbids: that rule is about a background job's own completion notification,
|
|
122
129
|
which does fire. Nothing at all notifies a pack between children, so the
|
|
123
130
|
heartbeat is the only guaranteed wake a Claude pack has. Do not delete it.
|
|
131
|
+
Locally, every wake — heartbeat or not — starts with
|
|
132
|
+
`checkout verify --card <parent-slug>` before it touches the tree, and a
|
|
133
|
+
result other than `ok` stops the pack (see
|
|
134
|
+
[checkout-claim.md](checkout-claim.md)).
|
|
124
135
|
|
|
125
136
|
**Codex CLI.** There is no `ScheduleWakeup` and no `/loop`. The thread goal
|
|
126
137
|
created per the SKILL.md *Goal and finish line* section is the continuation:
|
|
@@ -153,7 +164,10 @@ parent chat, skip that child, and take the next ready one.
|
|
|
153
164
|
Then follow [task-path.md](task-path.md), with one substitution: the child
|
|
154
165
|
branches from the **pack branch**, not `dev`, and its PR's base is the pack
|
|
155
166
|
branch. Name that base explicitly — the default is `dev`, and the two surfaces
|
|
156
|
-
spell the argument differently (`base:` locally, `baseBranch:` in a pod).
|
|
167
|
+
spell the argument differently (`base:` locally, `baseBranch:` in a pod). The
|
|
168
|
+
pack already holds the checkout claim, so the task path's claim steps become
|
|
169
|
+
`acquire --card <parent-slug> --branch <child-branch>` after you cut the child
|
|
170
|
+
branch, and there is no release at the child's finish line.
|
|
157
171
|
|
|
158
172
|
> **Environment — a pod has no child branches and no child PRs.** Everything
|
|
159
173
|
> above describes the LOCAL model, where each child gets its own branch and its
|
|
@@ -261,7 +275,8 @@ leave the branch pushed, and stop.
|
|
|
261
275
|
`base:` `dev`. The parent moves to ReviewPR. Post the pack summary: what
|
|
262
276
|
shipped per child, how it was verified, what reviewers should look at.
|
|
263
277
|
4. Confirm CI started. **Never approve or merge this PR** — it is the one that
|
|
264
|
-
gets independent review.
|
|
278
|
+
gets independent review. Locally, release the checkout:
|
|
279
|
+
`checkout release --card <parent-slug>`.
|
|
265
280
|
|
|
266
281
|
## Parked protocol
|
|
267
282
|
|
|
@@ -269,4 +284,6 @@ After two genuinely different failed approaches on a child, or a decision only
|
|
|
269
284
|
the user can make: post the reason and the specific question to the child AND
|
|
270
285
|
parent chats, set the child back to `Open`, restore the tree, and take the next
|
|
271
286
|
child whose dependency chain does not run through the parked one. Everything
|
|
272
|
-
remaining blocked →
|
|
287
|
+
remaining blocked → restore the tree, release the checkout claim locally
|
|
288
|
+
(`checkout release --card <parent-slug>`), report, and stop; the user's reply
|
|
289
|
+
is the un-park signal, and the resumed run acquires again.
|
|
@@ -13,11 +13,14 @@ Re-confirm via `mcp__conveyor__get_task` that the card is still claimable —
|
|
|
13
13
|
`Open` (or whatever status the user explicitly overrode), no assignee you do
|
|
14
14
|
not expect, no active session. Then:
|
|
15
15
|
|
|
16
|
-
1.
|
|
17
|
-
|
|
16
|
+
1. **Locally only:** `checkout acquire --card <slug>` — the checkout claim from
|
|
17
|
+
[checkout-claim.md](checkout-claim.md). `held` → touch nothing, flag it, and
|
|
18
|
+
wait; `dirty` → hard stop. A pod skips this step.
|
|
19
|
+
2. `mcp__conveyor__update_task` → `status: "InProgress"`
|
|
20
|
+
3. `mcp__conveyor__post_to_chat` → `[build] claimed — <where you are running>`
|
|
18
21
|
|
|
19
22
|
If the status changed under you between the read and the write, someone else
|
|
20
|
-
took it.
|
|
23
|
+
took it. Release the checkout claim and stop rather than compete.
|
|
21
24
|
|
|
22
25
|
## 2. Branch from the card's base — never blindly `dev`
|
|
23
26
|
|
|
@@ -29,6 +32,10 @@ git fetch origin <base>
|
|
|
29
32
|
git checkout -B <feat|fix|chore>/<slug> origin/<base>
|
|
30
33
|
```
|
|
31
34
|
|
|
35
|
+
Locally, record the branch on the checkout claim straight after the checkout:
|
|
36
|
+
`checkout acquire --card <slug> --branch <feat|fix|chore>/<slug>`. Do the
|
|
37
|
+
same after any later branch switch.
|
|
38
|
+
|
|
32
39
|
Two cases that are not a fresh branch:
|
|
33
40
|
|
|
34
41
|
- **The card already has a `githubBranch` with commits on origin.** Resume THAT
|
|
@@ -39,7 +46,8 @@ Two cases that are not a fresh branch:
|
|
|
39
46
|
absent even though the branch exists on the remote. Fetch it explicitly
|
|
40
47
|
(`git fetch origin <branch>`) rather than concluding the branch is missing —
|
|
41
48
|
and note that a failed `checkout` leaves you on your PREVIOUS branch, where a
|
|
42
|
-
follow-up `git push` will push the wrong thing.
|
|
49
|
+
follow-up `git push` will push the wrong thing. Locally, `checkout verify`
|
|
50
|
+
reports exactly that as `branch-moved`.
|
|
43
51
|
|
|
44
52
|
Reinstall dependencies if the lockfile changed.
|
|
45
53
|
|
|
@@ -62,6 +70,8 @@ Chat at milestones, not per step.
|
|
|
62
70
|
These are identical to the SKILL.md sections of the same names. In particular:
|
|
63
71
|
sync the base BEFORE the verification pass, pass `base:` explicitly to
|
|
64
72
|
`mcp__conveyor__create_pull_request`, and confirm CI started without waiting on
|
|
65
|
-
it.
|
|
73
|
+
it. Locally, `checkout verify --card <slug>` before the gate, each commit and
|
|
74
|
+
push, and the PR call.
|
|
66
75
|
|
|
67
76
|
Finish line: the card in ReviewPR with CI started. You do not merge it.
|
|
77
|
+
Locally, `checkout release --card <slug>` once CI has started.
|
|
@@ -47,12 +47,27 @@ adds only selection, claiming, cadence, and local-machine hygiene.
|
|
|
47
47
|
Clean tree before any branch switch is still a hard rule: dirty
|
|
48
48
|
`git status` at iteration start → touch nothing, report, and idle — the
|
|
49
49
|
resolution is the user committing or stashing, not a second checkout.
|
|
50
|
+
- **Hold the checkout claim while you use the tree.** WIP = 1 is per loop,
|
|
51
|
+
not per checkout: two loops, or a loop plus an interactive session, can
|
|
52
|
+
share this checkout, and a clean tree does not prove it is idle. Every tier
|
|
53
|
+
that touches git takes the claim first and gives it back at the finish line —
|
|
54
|
+
protocol in [conveyor-build's checkout-claim reference](../conveyor-build/references/checkout-claim.md).
|
|
50
55
|
- **Push early.** There is no pod WIP-autosync locally; committed-and-pushed is
|
|
51
56
|
the only durable state. Push the branch (`-u origin`) as soon as it exists.
|
|
52
57
|
|
|
53
58
|
## Iteration order
|
|
54
59
|
|
|
55
|
-
Each invocation does the FIRST of these that produces work, then paces
|
|
60
|
+
Each invocation does the FIRST of these that produces work, then paces.
|
|
61
|
+
|
|
62
|
+
**Checkout first.** Start every iteration with `checkout status`. `live` or
|
|
63
|
+
`corrupt` means another session holds this checkout: skip Recover, Babysit
|
|
64
|
+
fixes, and Claim (all three touch the tree), arm ONE background
|
|
65
|
+
`checkout wait` (see *Waking on board events*), and pace. Otherwise, each tier
|
|
66
|
+
below runs `checkout acquire --card <slug>` for its card before its first git
|
|
67
|
+
command or status write; `held` → stop that tier and arm the wait. The chat
|
|
68
|
+
marker names only the host, so a second loop on this host also matches
|
|
69
|
+
"my" marker in Recover — the checkout claim is what keeps it off a card another
|
|
70
|
+
live loop is still working.
|
|
56
71
|
|
|
57
72
|
1. **Recover** — a card with my `[local-loop] claimed` chat marker still
|
|
58
73
|
InProgress without a PR? Resume it. The branch may already exist locally or
|
|
@@ -89,13 +104,14 @@ Each invocation does the FIRST of these that produces work, then paces:
|
|
|
89
104
|
Skip `followParentStatus` mirror children. A blocker counts as met only
|
|
90
105
|
when merged-or-beyond (ReviewDev/ReviewLive/Complete) or Cancelled — a
|
|
91
106
|
blocker sitting in ReviewPR is NOT met until its PR merges.
|
|
92
|
-
2. Claim: re-confirm via `get_task` it is still Open,
|
|
93
|
-
`
|
|
107
|
+
2. Claim: re-confirm via `get_task` it is still Open, take the checkout claim
|
|
108
|
+
(`checkout acquire --card <slug>`; `held` → do not claim, arm the wait),
|
|
109
|
+
then `mcp__conveyor__update_task` → `status: "InProgress"`, then
|
|
94
110
|
`mcp__conveyor__post_to_chat`: `[local-loop] claimed — working locally on
|
|
95
111
|
<hostname>`. Claiming a PACK is different — never set the parent
|
|
96
112
|
InProgress: leave it Open and post the pack claim marker instead (see Pack
|
|
97
|
-
mode). Status changed under you → someone else took it;
|
|
98
|
-
candidate.
|
|
113
|
+
mode). Status changed under you → someone else took it; release the
|
|
114
|
+
checkout claim and take the next candidate.
|
|
99
115
|
3. Plan missing or failing the context-free-reader bar → don't wing it: post
|
|
100
116
|
what's missing to chat, leave the card Open, skip it.
|
|
101
117
|
|
|
@@ -115,15 +131,18 @@ This section adds only what the LOOP changes:
|
|
|
115
131
|
re-check the parent's status; if it went InProgress or ReviewPR, post
|
|
116
132
|
`[local-loop] parked: pack coordinator active — yielding`, leave the branch
|
|
117
133
|
pushed, and let the coordinator take over.
|
|
118
|
-
- **Do not wait on CI.** Confirm it started (read-only `gh pr checks`)
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
- **Do not wait on CI.** Confirm it started (read-only `gh pr checks`),
|
|
135
|
+
`checkout release --card <slug>`, and end the iteration — the Babysit tier
|
|
136
|
+
owns it from there. This is the loop's one real departure from a standalone
|
|
137
|
+
build, which stays with its PR. A Babysit fix re-acquires with that card and
|
|
138
|
+
releases again once the fix is pushed. A pack child's PR is the exception:
|
|
139
|
+
the pack keeps the claim until its finale PR opens.
|
|
121
140
|
|
|
122
141
|
**Parked protocol** — after 2 genuinely different failed approaches, or on a
|
|
123
142
|
decision only the user can make: post `[local-loop] parked: <reason + the
|
|
124
143
|
specific question>`, set status back to `"Open"`, restore the tree
|
|
125
|
-
(`git checkout dev`), move on. The user's
|
|
126
|
-
signal.
|
|
144
|
+
(`git checkout dev`), `checkout release --card <slug>`, move on. The user's
|
|
145
|
+
next chat reply is the un-park signal.
|
|
127
146
|
|
|
128
147
|
## Pack mode
|
|
129
148
|
|
|
@@ -136,9 +155,9 @@ dev→pack sync after every merge, cross-reference, then the finale parent PR
|
|
|
136
155
|
into dev. That reference is the source of truth for the procedure; this section
|
|
137
156
|
only defines how it embeds in the loop:
|
|
138
157
|
|
|
139
|
-
- The pack occupies the loop's single WIP slot
|
|
140
|
-
|
|
141
|
-
state.
|
|
158
|
+
- The pack occupies the loop's single WIP slot — and holds the checkout
|
|
159
|
+
claim under the parent's slug — from claim until the finale PR opens. Do not
|
|
160
|
+
interleave unrelated cards mid-pack — that thrashes branch state.
|
|
142
161
|
- Claim marker goes to the PARENT chat: `[local-loop] claimed — driving this
|
|
143
162
|
pack locally on <hostname>, pack branch <branch>`. The parent's status
|
|
144
163
|
stays Open (parked); the marker is the claim.
|
|
@@ -208,9 +227,12 @@ On wake, run a normal iteration and re-enumerate with
|
|
|
208
227
|
`mcp__conveyor__list_tasks`. By then the card may be claimed, cancelled, or
|
|
209
228
|
blocked by a dependency — never claim straight from the wait payload.
|
|
210
229
|
|
|
211
|
-
**Never arm a second wait.**
|
|
212
|
-
|
|
213
|
-
|
|
230
|
+
**Never arm a second wait.** At most ONE background wait of either kind —
|
|
231
|
+
`conveyor-wait` or `checkout wait` — runs at a time. On a wake where a wait
|
|
232
|
+
process is still in flight and nothing else changed, re-arm the fallback
|
|
233
|
+
`ScheduleWakeup` and end the turn. When the checkout is held, the
|
|
234
|
+
`checkout wait` is the one to arm: the board wait cannot tell you when the
|
|
235
|
+
checkout frees up.
|
|
214
236
|
|
|
215
237
|
## Pacing (dynamic /loop only)
|
|
216
238
|
|
|
@@ -219,7 +241,7 @@ Under `/loop` with no interval, end EVERY iteration with exactly one
|
|
|
219
241
|
|
|
220
242
|
| State | Delay | Reason should say |
|
|
221
243
|
|-------|-------|-------------------|
|
|
222
|
-
| A background gate/agent/conveyor-wait is in flight — its completion notification is the real wake | 1200–1800s fallback | "fallback while <gate> runs — its notification wakes me sooner" |
|
|
244
|
+
| A background gate/agent/conveyor-wait/checkout wait is in flight — its completion notification is the real wake | 1200–1800s fallback | "fallback while <gate> runs — its notification wakes me sooner" |
|
|
223
245
|
| ANY actionable work exists: claimable cards or packs, a pack child to implement/merge, a PR still to open, red/pending CI, review comments | 60–90s | queue depth / which item is next |
|
|
224
246
|
| Queue enumerated as empty THIS iteration, all loop PRs green and quiet | 1200–1800s | queue empty; conveyor-wait armed, so this is only the fallback |
|
|
225
247
|
| Loop-fatal: MCP dead after 2 tries, dirty tree, broken repo | notify the user (PushNotification if available), then 1800s — or `stop: true` if continuing is unsafe | what is wrong |
|
|
@@ -249,7 +271,8 @@ depth each iteration so the user can offload manually.
|
|
|
249
271
|
- Not a pod: no sandbox, no WIP snapshots, and the dev DB + dev-server ports
|
|
250
272
|
are shared with the user's interactive sessions — no destructive
|
|
251
273
|
experiments, never reset the dev DB, reuse a running dev stack rather than
|
|
252
|
-
fighting over ports.
|
|
274
|
+
fighting over ports. The git checkout is shared the same way, which is why
|
|
275
|
+
the loop takes turns on it through the checkout claim.
|
|
253
276
|
- Not a parallel executor: one card (or one pack) at a time is the point (the
|
|
254
277
|
full machine per gate). Backlogged? That is what claudespaces — or the
|
|
255
278
|
offload valve — are for.
|
|
@@ -25,10 +25,11 @@ most two packs.
|
|
|
25
25
|
**Finish line:** the report — verdict on its first line — posted to the
|
|
26
26
|
conversation, and to the release card's chat when a release card exists.
|
|
27
27
|
|
|
28
|
-
**A turn may end only when:** (a) the finish line is true,
|
|
29
|
-
`AskUserQuestion` is pending because the release scope is genuinely ambiguous
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
**A turn may end only when:** (a) the finish line is true, (b) an
|
|
29
|
+
`AskUserQuestion` is pending because the release scope is genuinely ambiguous,
|
|
30
|
+
or (c) agents you launched are still running; their completion notices resume
|
|
31
|
+
the run. Anything else — a ledger with blank rows, findings listed but not
|
|
32
|
+
filed, packs created with no report — is a stalled run, not a paused one.
|
|
32
33
|
|
|
33
34
|
**Runtime:** *Codex CLI* — before step 0, create a thread goal with your goal
|
|
34
35
|
tool: "Post a release verdict for <release> with every change evaluated and
|
|
@@ -63,6 +64,9 @@ the finish line above is the loop.
|
|
|
63
64
|
access and any release checklist live in the host repo's CLAUDE.md. A wrapper
|
|
64
65
|
skill may add dimensions (see the end of this file); it cannot lower the
|
|
65
66
|
evidence bar.
|
|
67
|
+
- **The fan-out rules are not project policy.** They follow from how the
|
|
68
|
+
harness behaves and from account limits, so no project can relax them. A
|
|
69
|
+
wrapper may LOWER the concurrency cap; it may never raise it or lift a rule.
|
|
66
70
|
|
|
67
71
|
## 0 — Discover the project
|
|
68
72
|
|
|
@@ -131,9 +135,21 @@ Anchor to a concrete diff before reading any code.
|
|
|
131
135
|
5. **Build the coverage ledger** — one row per PR or card, one row per
|
|
132
136
|
dimension in step 2. Every row ends `clean`, `finding`, or
|
|
133
137
|
`not reviewable (why)`. "Evaluate everything" means the ledger has no blank
|
|
134
|
-
rows when you finish.
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
rows when you finish.
|
|
139
|
+
|
|
140
|
+
**Fanning out.** On a large release, split the ledger across subagents per
|
|
141
|
+
area. Give each the ground rules and the evidence bar verbatim. Three rules
|
|
142
|
+
hold, and the mechanics are in [references/fan-out.md](references/fan-out.md):
|
|
143
|
+
|
|
144
|
+
1. **The hand-back is the only report channel.** The harness refuses report
|
|
145
|
+
files written by subagents. Each agent's final message is its report, and
|
|
146
|
+
you save each one to the run directory the moment it arrives.
|
|
147
|
+
2. **Only you spawn agents.** Every agent prompt forbids the Agent, Task and
|
|
148
|
+
Workflow tools. Nested agents share your session limit.
|
|
149
|
+
3. **At most four agents in flight**, the adversarial agent included. Launch
|
|
150
|
+
the next slice when one hands back, never in fixed waves.
|
|
151
|
+
|
|
152
|
+
The ledger is still the completeness check, and it stays with you.
|
|
137
153
|
|
|
138
154
|
## 2 — Review every dimension
|
|
139
155
|
|
|
@@ -206,9 +222,12 @@ in it that mattered.
|
|
|
206
222
|
## 5 — The adversarial pass
|
|
207
223
|
|
|
208
224
|
Every candidate that reached step 4, **together with the fix you would
|
|
209
|
-
propose**, now faces a reviewer whose only job is to strike it. Use
|
|
210
|
-
subagent
|
|
211
|
-
|
|
225
|
+
propose**, now faces a reviewer whose only job is to strike it. Use one fresh
|
|
226
|
+
subagent for all candidates, under the fan-out rules. Write the candidates,
|
|
227
|
+
with their evidence, priority and proposed fix, to `candidates.md` in the run
|
|
228
|
+
directory; with no fan-out, put them in the prompt instead. The agent argues
|
|
229
|
+
for striking each against the tests below and hands back one verdict per
|
|
230
|
+
candidate. Where subagents are unavailable, switch stance explicitly and
|
|
212
231
|
write the case against each finding before deciding.
|
|
213
232
|
|
|
214
233
|
Strike when:
|
|
@@ -344,6 +363,10 @@ moves.
|
|
|
344
363
|
already happened.
|
|
345
364
|
- Diagnosing an incident here. An incident the release implicates is
|
|
346
365
|
evidence; its root cause belongs to `/conveyor-triage`.
|
|
366
|
+
- Telling subagents to write findings files. The harness refuses them. The
|
|
367
|
+
hand-back is the report, and saving it is your job.
|
|
368
|
+
- Letting a reviewer fan out. Its agents share your session limit, and a limit
|
|
369
|
+
hit kills every agent in flight with its unsaved work.
|
|
347
370
|
|
|
348
371
|
## Wrapping this skill for one project
|
|
349
372
|
|
|
@@ -364,6 +387,10 @@ evidence bar and adversarial pass.
|
|
|
364
387
|
re-check before concluding.
|
|
365
388
|
- The log tools appear only for a project with the matching integration. If
|
|
366
389
|
they are absent, the project has none; say so instead of hunting.
|
|
390
|
+
- `You've hit your session limit · resets <time>` (HTTP 429) is the account's
|
|
391
|
+
five-hour usage window, shared by every agent and by you. Nothing runs until
|
|
392
|
+
the reset, so do not retry. Resume per
|
|
393
|
+
[references/fan-out.md](references/fan-out.md).
|
|
367
394
|
|
|
368
395
|
## Improve This Skill
|
|
369
396
|
|
|
@@ -287,3 +287,9 @@ Evidence tools:
|
|
|
287
287
|
|
|
288
288
|
A wrapper may add dimensions and name tools. It may not lower the evidence
|
|
289
289
|
bar, skip the adversarial pass, or change what the priority levels mean.
|
|
290
|
+
|
|
291
|
+
A wrapper also may not relax the fan-out rules in
|
|
292
|
+
[fan-out.md](fan-out.md): subagents hand back text and write no report files,
|
|
293
|
+
only the orchestrator spawns agents, and at most four run at once. These
|
|
294
|
+
follow from how the harness behaves and from account limits, not from project
|
|
295
|
+
policy. A wrapper may LOWER the cap; it may never raise it or lift a rule.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Fanning out — the mechanics
|
|
2
|
+
|
|
3
|
+
How the orchestrator of a [release review](../SKILL.md) splits the ledger
|
|
4
|
+
across subagents without losing their work. SKILL.md states the three rules;
|
|
5
|
+
this file is how to follow them.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
Two limits broke earlier runs. Neither is project policy. Both come from how
|
|
10
|
+
the harness and the account behave, so a wrapper skill may lower the cap
|
|
11
|
+
below but never raise it or lift a rule.
|
|
12
|
+
|
|
13
|
+
1. **The harness refuses report files from subagents.** Claude Code's Write
|
|
14
|
+
tool refuses a subagent `.md` whose name starts with REPORT, SUMMARY,
|
|
15
|
+
FINDINGS or ANALYSIS:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Subagents should return findings as text, not write report files.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This guard is deliberate. Do not route around it with another file name or
|
|
22
|
+
a Bash heredoc. The subagent's final message is the report.
|
|
23
|
+
|
|
24
|
+
2. **Every agent draws on one account session limit.** The limit is a single
|
|
25
|
+
five-hour usage window. Every subagent and the orchestrator share it. When
|
|
26
|
+
it runs out, every call fails at once:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
You've hit your session limit · resets 9:20pm
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
(HTTP 429, `rateLimitType: "five_hour"`). Every agent in flight dies, and
|
|
33
|
+
its unsaved work dies with it. The orchestrator is down too, until the
|
|
34
|
+
reset. A run with 13 agents in flight lost 10; a run with about 10 in flight
|
|
35
|
+
(one reviewer spawned 3 of its own) lost 5. Runs at 7 and at 4 lost none.
|
|
36
|
+
|
|
37
|
+
No cap makes a run safe, because the limit is account-wide. The cap bounds
|
|
38
|
+
what one hit destroys. The no-nesting rule and the tool-call budget cut what a
|
|
39
|
+
run spends.
|
|
40
|
+
|
|
41
|
+
## The run directory
|
|
42
|
+
|
|
43
|
+
`/tmp/conveyor-release-review/<label-slug>/`, where `<label-slug>` is the
|
|
44
|
+
release label in kebab case (`release-2026-09-30`, `pre-release-2026-09-30`).
|
|
45
|
+
Create it before the first launch. It holds:
|
|
46
|
+
|
|
47
|
+
- `brief.md` — the shared part of every agent prompt: the brief block below,
|
|
48
|
+
the ground rules and the evidence bar, verbatim.
|
|
49
|
+
- `ledger.md` — the coverage ledger. Its first line names the run directory,
|
|
50
|
+
so a resumed or compacted session finds it again.
|
|
51
|
+
- `<slice>.md` — one file per agent, holding that agent's hand-back.
|
|
52
|
+
|
|
53
|
+
**On each completion notice, in this order:**
|
|
54
|
+
|
|
55
|
+
1. Write the hand-back verbatim to `<slice>.md`. Do this before you read it
|
|
56
|
+
closely, and before anything else can fail.
|
|
57
|
+
2. Mark the ledger rows the hand-back covers.
|
|
58
|
+
3. Launch the next slice, if one is waiting.
|
|
59
|
+
|
|
60
|
+
The saved files are the durable record. They survive compaction and a resume,
|
|
61
|
+
and the adversarial agent reads findings from them by path.
|
|
62
|
+
|
|
63
|
+
## The brief block
|
|
64
|
+
|
|
65
|
+
Paste this verbatim into every agent prompt, area and adversarial alike:
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
1. Your final message is your report. Do not write report, summary or
|
|
69
|
+
findings files, and do not route a refused write through Bash or another
|
|
70
|
+
file name. The orchestrator saves your final message.
|
|
71
|
+
2. Do not spawn agents or run workflows (no Agent, Task or Workflow tool).
|
|
72
|
+
The orchestrator owns all fan-out.
|
|
73
|
+
3. Budget: about 150 tool calls. At the budget, stop and return what you
|
|
74
|
+
have, listing what you did not reach as "not reviewed".
|
|
75
|
+
4. Already settled, do not re-derive: <list, or "nothing yet">.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then give the hand-back format:
|
|
79
|
+
|
|
80
|
+
- **Coverage** — one row per PR or card: `clean`, `finding`, or
|
|
81
|
+
`not reviewable (why)`.
|
|
82
|
+
- **Findings** — each with its priority, the PR that introduced it, the
|
|
83
|
+
evidence grade, the exact command and its result, the smallest fix, and the
|
|
84
|
+
strongest case for striking it.
|
|
85
|
+
- **Unverified** — each with the query that would settle it.
|
|
86
|
+
- **Struck** — each with its reason.
|
|
87
|
+
- **Deploy-checklist items.**
|
|
88
|
+
|
|
89
|
+
## Scheduling
|
|
90
|
+
|
|
91
|
+
- **At most 4 agents in flight**, the adversarial agent included.
|
|
92
|
+
- **Launch as slots free up**, not in fixed waves. When one agent hands back
|
|
93
|
+
and its file is saved, launch the next slice.
|
|
94
|
+
- **One adversarial agent.** Launch it only after every area file exists and
|
|
95
|
+
you have finished steps 3 and 4. First write `candidates.md`: every
|
|
96
|
+
candidate that reached step 4, with its evidence, its priority and the fix
|
|
97
|
+
you would propose. The area files hold raw hand-backs, so they miss your own
|
|
98
|
+
candidates, your grading and your fixes. The agent reads `candidates.md`,
|
|
99
|
+
opens the area files only for detail, argues for striking each candidate,
|
|
100
|
+
and hands back one verdict per candidate. Save its hand-back to
|
|
101
|
+
`adversarial.md`.
|
|
102
|
+
- While agents run, end the turn. Their completion notices resume the run.
|
|
103
|
+
|
|
104
|
+
## When an agent dies
|
|
105
|
+
|
|
106
|
+
**A session limit** (`session limit · resets <time>`):
|
|
107
|
+
|
|
108
|
+
1. You are down too. Nothing runs until the reset, and an immediate retry
|
|
109
|
+
fails. Do not retry.
|
|
110
|
+
2. After the reset, reread the run directory and the ledger.
|
|
111
|
+
3. Relaunch only the slices with no `<slice>.md`. Narrow each one if you can,
|
|
112
|
+
and give each an already-settled list built from the saved files.
|
|
113
|
+
4. Never relaunch above the cap that hit the limit. If 4 in flight hit it,
|
|
114
|
+
relaunch at 3 or fewer.
|
|
115
|
+
|
|
116
|
+
**Any other error** (overloaded, or a rate limit with no reset time):
|
|
117
|
+
|
|
118
|
+
1. Wait a minute or two, then relaunch that one slice once.
|
|
119
|
+
2. If it fails again, review the slice yourself, or mark its ledger rows
|
|
120
|
+
`not reviewable (agent failed twice: <error>)`.
|
|
121
|
+
|
|
122
|
+
**Always:**
|
|
123
|
+
|
|
124
|
+
- Never relaunch a slice whose file exists.
|
|
125
|
+
- Never relaunch a whole wave.
|
|
126
|
+
|
|
127
|
+
## If you were spawned as a subagent yourself
|
|
128
|
+
|
|
129
|
+
You are not the orchestrator. Do not fan out. Work the ledger yourself, and
|
|
130
|
+
return your report as your final message.
|
|
@@ -110,6 +110,12 @@ You have write access. Use it in proportion:
|
|
|
110
110
|
|
|
111
111
|
- **Small and unambiguous** → fix it, commit, push, then re-review your own
|
|
112
112
|
change as part of the diff. After pushing, wait for CI before approving.
|
|
113
|
+
**Locally**, a fix checks out the PR branch in a checkout other sessions may
|
|
114
|
+
share. Run `checkout acquire --card <slug>` before the checkout (`held` →
|
|
115
|
+
flag the fix instead of making it), `checkout acquire --card <slug> --branch
|
|
116
|
+
<pr-branch>` after it, and `checkout release --card <slug>` once the fix is
|
|
117
|
+
pushed and the tree is restored — protocol in
|
|
118
|
+
[conveyor-build's checkout-claim reference](../conveyor-build/references/checkout-claim.md).
|
|
113
119
|
- **Larger, or a judgment call the author should make** → flag it in the
|
|
114
120
|
verdict with the file, the line, what is wrong, and a suggested direction.
|
|
115
121
|
|
|
@@ -215,6 +215,13 @@ sync-spawned duplicate.
|
|
|
215
215
|
origin <branch>` and `git merge-base --is-ancestor` before rebuilding
|
|
216
216
|
anything locally — platform autosync may have already pushed for you, and a
|
|
217
217
|
dirty tree may belong to a concurrent session.
|
|
218
|
+
- **Local sessions take turns on one checkout.** `conveyor-build`,
|
|
219
|
+
`conveyor-local-loop` and local `conveyor-review` fixes hold a checkout
|
|
220
|
+
claim, so a second session waits instead of switching branches underneath
|
|
221
|
+
the first. `conveyor-skills checkout status` shows who holds it (card,
|
|
222
|
+
branch, host, heartbeat age). `conveyor-skills checkout release --force` is
|
|
223
|
+
the human override for a wedged claim — an agent runs it only when the user
|
|
224
|
+
says so. Protocol: `conveyor-build/references/checkout-claim.md`.
|
|
218
225
|
|
|
219
226
|
## Improve This Skill
|
|
220
227
|
|