@rallycry/conveyor-agent 10.13.72 → 11.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,11 +30,11 @@ import {
30
30
  statWorkspacePath,
31
31
  updateRemoteToken,
32
32
  verifyGitCredential
33
- } from "./chunk-LSZ2KLJY.js";
33
+ } from "./chunk-N4WSUTGV.js";
34
34
  import {
35
35
  registerBootMilestoneSocketFallback,
36
36
  reportBootMilestone
37
- } from "./chunk-WMMBAKPE.js";
37
+ } from "./chunk-GL2DIQEQ.js";
38
38
  import {
39
39
  LoopLagMonitor,
40
40
  loopStatusForRunnerStatus
@@ -78,16 +78,140 @@ import {
78
78
  sleep
79
79
  } from "./chunk-W4LZ7R6Z.js";
80
80
 
81
- // src/connection/auth-errors.ts
82
- function isPermissionDeniedError(err) {
83
- const message = err instanceof Error ? err.message : String(err);
84
- return /insufficient permissions|authentication required/i.test(message);
85
- }
86
-
87
- // ../shared/dist/chunk-6RHVH33O.js
81
+ // ../shared/dist/chunk-OKJPFFQI.js
88
82
  var CARD_DESCRIPTION_MAX = 255;
89
83
  var CARD_DESCRIPTION_LIMIT_MESSAGE = `Card descriptions are capped at ${CARD_DESCRIPTION_MAX} characters \u2014 write 1-2 plain sentences a non-engineer can read; put technical detail in the plan or card chat.`;
90
84
  var CARD_DESCRIPTION_FIELD_HINT = `max ${CARD_DESCRIPTION_MAX} chars, 1-2 plain sentences a non-engineer can read \u2014 put technical detail in the plan`;
85
+ var SEVERITY_ENUM = [
86
+ "DEBUG",
87
+ "INFO",
88
+ "NOTICE",
89
+ "WARNING",
90
+ "ERROR",
91
+ "CRITICAL",
92
+ "ALERT",
93
+ "EMERGENCY"
94
+ ];
95
+ var MAX_LINE_CHARS = 400;
96
+ var DEFAULT_SINCE_MINUTES = 60;
97
+ function truncateLine(text) {
98
+ const oneLine = text.replace(/\s*\n\s*/g, " \u23CE ");
99
+ if (oneLine.length <= MAX_LINE_CHARS) return oneLine;
100
+ const overflow = oneLine.length - MAX_LINE_CHARS;
101
+ return `${oneLine.slice(0, MAX_LINE_CHARS)}\u2026[+${overflow}c]`;
102
+ }
103
+ function entrySource(entry) {
104
+ return entry.resource.service_name ?? entry.resource.pod_name ?? entry.resource.database_id ?? entry.resourceType ?? "-";
105
+ }
106
+ var PAYLOAD_SKIP_KEYS = /* @__PURE__ */ new Set(["message", "severity", "timestamp", "level", "stack"]);
107
+ var PAYLOAD_PRIORITY = [
108
+ "error",
109
+ "outcome",
110
+ "serviceName",
111
+ "methodName",
112
+ "userId",
113
+ "taskId",
114
+ "sessionId",
115
+ "workspaceId",
116
+ "projectId",
117
+ "durationMs"
118
+ ];
119
+ var PAYLOAD_VALUE_MAX_CHARS = 160;
120
+ function compactPayloadValue(value) {
121
+ const raw = typeof value === "string" ? value : JSON.stringify(value);
122
+ const flat = (raw ?? "undefined").replace(/\s+/g, " ");
123
+ const quoted = typeof value === "string" && /[\s"]/.test(flat) ? JSON.stringify(flat) : flat;
124
+ return quoted.length > PAYLOAD_VALUE_MAX_CHARS ? `${quoted.slice(0, PAYLOAD_VALUE_MAX_CHARS)}\u2026` : quoted;
125
+ }
126
+ function formatPayloadSuffix(payloadJson) {
127
+ if (!payloadJson) return "";
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(payloadJson);
131
+ } catch {
132
+ return "";
133
+ }
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
135
+ const obj = parsed;
136
+ const rank = (key) => {
137
+ const i = PAYLOAD_PRIORITY.indexOf(key);
138
+ return i === -1 ? PAYLOAD_PRIORITY.length : i;
139
+ };
140
+ 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])}`);
141
+ return parts.length > 0 ? ` | ${parts.join(" ")}` : "";
142
+ }
143
+ function formatLogEntryLine(entry) {
144
+ const httpPrefix = entry.httpRequest?.status ? `http ${entry.httpRequest.status} ${entry.httpRequest.method ?? ""} ${entry.httpRequest.url ?? ""}`.trim() + " \u2014 " : "";
145
+ return `${entry.timestamp} ${entry.severity.padEnd(7)} [${entrySource(entry)}] ${truncateLine(
146
+ `${httpPrefix}${entry.message}${formatPayloadSuffix(entry.payload)}`
147
+ )}`;
148
+ }
149
+ async function runQueryGcpLogs(port, params, now = Date.now) {
150
+ const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
151
+ const result = await port.queryGcpLogs({
152
+ projectId: params.projectId,
153
+ env: params.env,
154
+ severity: params.severity,
155
+ services: params.services,
156
+ sqlInstances: params.sqlInstances,
157
+ allServices: params.allServices,
158
+ search: params.search,
159
+ filter: params.filter,
160
+ startTime,
161
+ endTime: params.endTime,
162
+ limit: params.limit,
163
+ pageToken: params.pageToken
164
+ });
165
+ if (result.error) return result.error;
166
+ const header = [
167
+ `env=${params.env ?? "prod"}`,
168
+ `window=${startTime}\u2192${params.endTime ?? "now"}`,
169
+ ...params.severity ? [`minSeverity=${params.severity}`] : [],
170
+ `scope=${result.scopedServices ? `[${result.scopedServices.join(", ")}]` : "all"}`,
171
+ `entries=${result.entries.length}`
172
+ ].join(" ");
173
+ const lines = result.entries.map(formatLogEntryLine);
174
+ const footer = result.nextPageToken ? [`-- more available: pass pageToken="${result.nextPageToken}" to continue`] : [];
175
+ if (lines.length === 0) {
176
+ return [
177
+ header,
178
+ "(no matching log entries \u2014 widen the window, lower minSeverity, or drop filters)"
179
+ ].join("\n");
180
+ }
181
+ return [header, ...lines, ...footer].join("\n");
182
+ }
183
+ async function runQueryGrafanaLogs(port, params, now = Date.now) {
184
+ const startTime = params.startTime ?? new Date(now() - (params.sinceMinutes ?? DEFAULT_SINCE_MINUTES) * 6e4).toISOString();
185
+ const result = await port.queryGrafanaLogs({
186
+ projectId: params.projectId,
187
+ env: params.env,
188
+ level: params.level,
189
+ services: params.services,
190
+ search: params.search,
191
+ logql: params.logql,
192
+ startTime,
193
+ endTime: params.endTime,
194
+ limit: params.limit
195
+ });
196
+ if (result.error) return result.error;
197
+ const header = [
198
+ `env=${params.env ?? "prod"}`,
199
+ `window=${startTime}\u2192${params.endTime ?? "now"}`,
200
+ ...params.level ? [`minLevel=${params.level}`] : [],
201
+ ...result.logql ? [`logql=${truncateLine(result.logql)}`] : [],
202
+ `entries=${result.entries.length}`
203
+ ].join(" ");
204
+ const lines = result.entries.map(formatLogEntryLine);
205
+ const oldest = result.entries.map((e) => e.timestamp).sort()[0];
206
+ const footer = result.hasMore ? [`-- hit the limit: older lines exist \u2014 pass endTime="${oldest}" to page further back`] : [];
207
+ if (lines.length === 0) {
208
+ return [
209
+ header,
210
+ "(no matching log entries \u2014 widen the window, lower minLevel, or drop filters)"
211
+ ].join("\n");
212
+ }
213
+ return [header, ...lines, ...footer].join("\n");
214
+ }
91
215
 
92
216
  // ../shared/dist/index.js
93
217
  import { z } from "zod";
@@ -97,6 +221,8 @@ import { z as z4 } from "zod";
97
221
  import { z as z5 } from "zod";
98
222
  import { z as z6 } from "zod";
99
223
  import { z as z7 } from "zod";
224
+ import { z as z8 } from "zod";
225
+ import { z as z9 } from "zod";
100
226
  var EXTERNAL_AGENT_MESSAGE_SOURCE = "external_agent";
101
227
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
102
228
  var DEFAULT_OPUS_MODEL = "claude-opus-5";
@@ -282,12 +408,24 @@ var ACTIVE_WORK_STATUSES = [
282
408
  "Complete"
283
409
  ];
284
410
  var IDENTIFIED_WORK_STATUSES = ["Open", ...ACTIVE_WORK_STATUSES];
411
+ var DEFAULT_TASK_STATUS_COLOR = "#a8a29e";
412
+ var DEFAULT_TASK_STATUS_COLOR_INT = Number.parseInt(DEFAULT_TASK_STATUS_COLOR.slice(1), 16);
285
413
  var MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024;
286
414
  var MAX_FILE_TAGS = 5;
287
415
  var MAX_FILE_TAG_LENGTH = 100;
288
416
  var EMBED_THRESHOLD_IMAGES = 5 * 1024 * 1024;
289
417
  var EMBED_THRESHOLD_TEXT = 2 * 1024 * 1024;
290
418
  var IDLE_HEARTBEAT_MS = 90 * 1e3;
419
+ var RUNNER_MODES = [
420
+ "task",
421
+ "plan",
422
+ "pm",
423
+ "code-review",
424
+ "adhoc",
425
+ "pack",
426
+ "shell",
427
+ "serving"
428
+ ];
291
429
  var TurnEndToolCallSchema = z3.object({
292
430
  tool: z3.string(),
293
431
  input: z3.string().optional(),
@@ -845,6 +983,9 @@ var ReportReviewSpawnFailureRequestSchema = z4.object({
845
983
  reviewSessionId: z4.string(),
846
984
  error: z4.string().max(2e3).optional()
847
985
  });
986
+ var ReportBuilderSpawnFailureRequestSchema = ReportReviewSpawnFailureRequestSchema.omit({
987
+ reviewSessionId: true
988
+ }).extend({ buildSessionId: z4.string() });
848
989
  var RequestWorkspaceRecycleRequestSchema = z4.object({
849
990
  sessionId: z4.string(),
850
991
  reason: z4.string().max(2e3)
@@ -1497,12 +1638,46 @@ var CreateProjectSuggestionRequestSchema = z5.object({
1497
1638
  tagNames: z5.array(z5.string()).optional(),
1498
1639
  requestingUserId: z5.string().optional()
1499
1640
  });
1641
+ var ListProjectChannelsRequestSchema = z6.object({
1642
+ projectId: z6.string()
1643
+ });
1644
+ var READ_CHANNEL_MESSAGES_MAX_LIMIT = 50;
1645
+ var ReadChannelMessagesRequestSchema = z6.object({
1646
+ projectId: z6.string(),
1647
+ channelId: z6.string().min(1).max(200),
1648
+ limit: z6.number().int().min(1).max(READ_CHANNEL_MESSAGES_MAX_LIMIT).optional(),
1649
+ /** Provider-native cursor: return messages OLDER than this one. */
1650
+ before: z6.string().max(100).optional(),
1651
+ /** Provider-native cursor: return messages NEWER than this one. */
1652
+ after: z6.string().max(100).optional(),
1653
+ /**
1654
+ * Read one thread instead of the channel surface. Slack calls this a
1655
+ * `thread_ts`; on Discord it is the thread channel's id. One field for both,
1656
+ * because a caller holding a `threadTs` from a previous read should not have
1657
+ * to know which provider produced it.
1658
+ */
1659
+ threadTs: z6.string().max(100).optional()
1660
+ });
1661
+ var POST_CHANNEL_MESSAGE_MAX_CHARS = 1800;
1662
+ var PostChannelMessageRequestSchema = z6.object({
1663
+ projectId: z6.string(),
1664
+ channelId: z6.string().min(1).max(200),
1665
+ text: z6.string().min(1).max(POST_CHANNEL_MESSAGE_MAX_CHARS),
1666
+ /** Reply inside a thread rather than to the channel. */
1667
+ threadTs: z6.string().max(100).optional()
1668
+ });
1669
+ var GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS = 90;
1670
+ var GetProjectAnalyticsSummaryRequestSchema = z6.object({
1671
+ projectId: z6.string(),
1672
+ rangeDays: z6.number().int().min(1).max(GET_ANALYTICS_SUMMARY_MAX_RANGE_DAYS).optional(),
1673
+ campaign: z6.string().max(200).optional()
1674
+ });
1500
1675
  var SHA_PATTERN = /^[0-9a-f]{40}$/i;
1501
- var ReviewGuideFileReferenceSchema = z6.object({
1502
- path: z6.string().min(1).max(500),
1503
- startLine: z6.number().int().positive().max(1e6).optional(),
1504
- endLine: z6.number().int().positive().max(1e6).optional(),
1505
- hunkHeader: z6.string().min(1).max(300).optional()
1676
+ var ReviewGuideFileReferenceSchema = z7.object({
1677
+ path: z7.string().min(1).max(500),
1678
+ startLine: z7.number().int().positive().max(1e6).optional(),
1679
+ endLine: z7.number().int().positive().max(1e6).optional(),
1680
+ hunkHeader: z7.string().min(1).max(300).optional()
1506
1681
  }).strict().superRefine((value, ctx) => {
1507
1682
  if (value.endLine !== void 0 && value.startLine === void 0) {
1508
1683
  ctx.addIssue({
@@ -1519,19 +1694,19 @@ var ReviewGuideFileReferenceSchema = z6.object({
1519
1694
  });
1520
1695
  }
1521
1696
  });
1522
- var ReviewGuideSectionSchema = z6.object({
1523
- title: z6.string().min(1).max(160),
1524
- explanation: z6.string().min(1).max(2e3),
1525
- classification: z6.enum(["core", "supporting"]).optional(),
1526
- files: z6.array(ReviewGuideFileReferenceSchema).min(1).max(20)
1697
+ var ReviewGuideSectionSchema = z7.object({
1698
+ title: z7.string().min(1).max(160),
1699
+ explanation: z7.string().min(1).max(2e3),
1700
+ classification: z7.enum(["core", "supporting"]).optional(),
1701
+ files: z7.array(ReviewGuideFileReferenceSchema).min(1).max(20)
1527
1702
  }).strict();
1528
- var ReviewGuideContentSchema = z6.object({
1529
- overview: z6.string().min(1).max(3e3),
1530
- sections: z6.array(ReviewGuideSectionSchema).min(1).max(12)
1703
+ var ReviewGuideContentSchema = z7.object({
1704
+ overview: z7.string().min(1).max(3e3),
1705
+ sections: z7.array(ReviewGuideSectionSchema).min(1).max(12)
1531
1706
  }).strict();
1532
1707
  var PublishReviewGuideRequestSchema = ReviewGuideContentSchema.extend({
1533
- sessionId: z6.string().min(1),
1534
- reviewedSha: z6.string().regex(SHA_PATTERN, "reviewedSha must be a 40-character commit SHA")
1708
+ sessionId: z7.string().min(1),
1709
+ reviewedSha: z7.string().regex(SHA_PATTERN, "reviewedSha must be a 40-character commit SHA")
1535
1710
  }).strict();
1536
1711
  var CONTEXT_LINK_LOCATOR_MAX = 300;
1537
1712
  var TEST_TITLE = /\b(?:it|test|describe)(?:\.\w+)?\s*\(\s*(['"`])((?:(?!\1)[\s\S])*)\1/g;
@@ -1548,130 +1723,130 @@ function locatorMatchesContent(content, locatorType, locator) {
1548
1723
  var TAG_DESCRIPTION_MAX = CARD_DESCRIPTION_MAX;
1549
1724
  var TAG_OVERVIEW_MAX = 32e3;
1550
1725
  var TAG_REASON_MAX = 500;
1551
- var ProjectTagContextPathSchema = z7.object({
1552
- type: z7.enum(["rule", "doc", "file", "folder"]),
1553
- path: z7.string().min(1).max(500),
1554
- label: z7.string().max(100).optional(),
1726
+ var ProjectTagContextPathSchema = z8.object({
1727
+ type: z8.enum(["rule", "doc", "file", "folder"]),
1728
+ path: z8.string().min(1).max(500),
1729
+ label: z8.string().max(100).optional(),
1555
1730
  /** Verified-link tether — text that must keep existing in the file. */
1556
- locator: z7.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional(),
1731
+ locator: z8.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional(),
1557
1732
  /** test = must appear in a real test/describe title; code = any substring. */
1558
- locatorType: z7.enum(["test", "code"]).optional()
1733
+ locatorType: z8.enum(["test", "code"]).optional()
1559
1734
  }).refine((link) => link.locator === void 0 === (link.locatorType === void 0), {
1560
1735
  message: "locator and locatorType must be provided together"
1561
1736
  }).refine((link) => link.locator === void 0 || link.type !== "folder", {
1562
1737
  message: "folder links cannot carry a locator"
1563
1738
  });
1564
- var hexColor = z7.string().regex(/^#[0-9a-fA-F]{6}$/, "Expected #RRGGBB hex color");
1565
- var overviewPathSchema = z7.string().min(1).max(500).regex(/^[^\r\n]*$/, "Overview path cannot contain line breaks");
1566
- var CreateProjectTagRequestSchema = z7.object({
1567
- projectId: z7.string(),
1568
- name: z7.string().min(1).max(50),
1739
+ var hexColor = z8.string().regex(/^#[0-9a-fA-F]{6}$/, "Expected #RRGGBB hex color");
1740
+ var overviewPathSchema = z8.string().min(1).max(500).regex(/^[^\r\n]*$/, "Overview path cannot contain line breaks");
1741
+ var CreateProjectTagRequestSchema = z8.object({
1742
+ projectId: z8.string(),
1743
+ name: z8.string().min(1).max(50),
1569
1744
  color: hexColor.optional(),
1570
- description: z7.string().max(TAG_DESCRIPTION_MAX).optional(),
1571
- overview: z7.string().max(TAG_OVERVIEW_MAX).optional(),
1745
+ description: z8.string().max(TAG_DESCRIPTION_MAX).optional(),
1746
+ overview: z8.string().max(TAG_OVERVIEW_MAX).optional(),
1572
1747
  /** Source the overview from this repo file (stored overview stays as the pending fallback). */
1573
1748
  overviewPath: overviewPathSchema.optional(),
1574
- contextPaths: z7.array(ProjectTagContextPathSchema).max(20).optional(),
1749
+ contextPaths: z8.array(ProjectTagContextPathSchema).max(20).optional(),
1575
1750
  /** Parents to link at create time (multi-parent DAG). */
1576
- parentTagIds: z7.array(z7.string()).max(25).optional(),
1577
- requestingUserId: z7.string().optional()
1751
+ parentTagIds: z8.array(z8.string()).max(25).optional(),
1752
+ requestingUserId: z8.string().optional()
1578
1753
  });
1579
- var UpdateProjectTagRequestSchema = z7.object({
1580
- projectId: z7.string(),
1581
- tagId: z7.string(),
1582
- name: z7.string().min(1).max(50).optional(),
1754
+ var UpdateProjectTagRequestSchema = z8.object({
1755
+ projectId: z8.string(),
1756
+ tagId: z8.string(),
1757
+ name: z8.string().min(1).max(50).optional(),
1583
1758
  color: hexColor.optional(),
1584
- description: z7.string().max(TAG_DESCRIPTION_MAX).optional(),
1759
+ description: z8.string().max(TAG_DESCRIPTION_MAX).optional(),
1585
1760
  /** Full markdown glossary body; null clears it. Rejected while overviewPath is set. */
1586
- overview: z7.string().max(TAG_OVERVIEW_MAX).nullable().optional(),
1761
+ overview: z8.string().max(TAG_OVERVIEW_MAX).nullable().optional(),
1587
1762
  /** Repo file to source the overview from; null clears back to the stored overview. */
1588
1763
  overviewPath: overviewPathSchema.nullable().optional(),
1589
1764
  /** Full replacement of the tag's context links when provided. */
1590
- contextPaths: z7.array(ProjectTagContextPathSchema).max(20).optional(),
1765
+ contextPaths: z8.array(ProjectTagContextPathSchema).max(20).optional(),
1591
1766
  /** Full-set replacement of the tag's parent tags (multi-parent DAG). */
1592
- parentTagIds: z7.array(z7.string()).max(25).optional(),
1767
+ parentTagIds: z8.array(z8.string()).max(25).optional(),
1593
1768
  /** One-line revision provenance, recorded in the tag's history. */
1594
- reason: z7.string().max(TAG_REASON_MAX).optional(),
1769
+ reason: z8.string().max(TAG_REASON_MAX).optional(),
1595
1770
  /** Card the caller was working in — stamped into the revision history. */
1596
- taskId: z7.string().optional(),
1597
- requestingUserId: z7.string().optional()
1771
+ taskId: z8.string().optional(),
1772
+ requestingUserId: z8.string().optional()
1598
1773
  });
1599
- var PostToProjectChatRequestSchema = z7.object({
1600
- projectId: z7.string(),
1601
- content: z7.string().min(1).max(2e4),
1602
- requestingUserId: z7.string().optional(),
1774
+ var PostToProjectChatRequestSchema = z8.object({
1775
+ projectId: z8.string(),
1776
+ content: z8.string().min(1).max(2e4),
1777
+ requestingUserId: z8.string().optional(),
1603
1778
  /** Marks the post so the server can persist it beyond chat (tag-audit summaries land in tag history). */
1604
- kind: z7.enum(["tag_audit_summary"]).optional()
1605
- });
1606
- var StartTagAuditRequestSchema = z7.object({
1607
- projectId: z7.string(),
1608
- requestingUserId: z7.string().optional()
1609
- });
1610
- var StartTaskAuditRequestSchema = z7.object({
1611
- projectId: z7.string(),
1612
- taskIds: z7.array(z7.string()).min(1).max(20),
1613
- requestingUserId: z7.string().optional()
1614
- });
1615
- var GetActiveAuditSessionsRequestSchema = z7.object({
1616
- projectId: z7.string()
1617
- });
1618
- var ReportTaskAuditResultRequestSchema = z7.object({
1619
- projectId: z7.string(),
1620
- taskId: z7.string(),
1621
- summary: z7.string(),
1622
- turnGrades: z7.array(
1623
- z7.object({
1624
- turnIndex: z7.number(),
1625
- phase: z7.enum(["planning", "building", "human"]),
1626
- grade: z7.enum(["correct", "neutral", "blunder"]),
1627
- reasoning: z7.string(),
1628
- eventType: z7.string(),
1629
- eventSummary: z7.string()
1779
+ kind: z8.enum(["tag_audit_summary"]).optional()
1780
+ });
1781
+ var StartTagAuditRequestSchema = z8.object({
1782
+ projectId: z8.string(),
1783
+ requestingUserId: z8.string().optional()
1784
+ });
1785
+ var StartTaskAuditRequestSchema = z8.object({
1786
+ projectId: z8.string(),
1787
+ taskIds: z8.array(z8.string()).min(1).max(20),
1788
+ requestingUserId: z8.string().optional()
1789
+ });
1790
+ var GetActiveAuditSessionsRequestSchema = z8.object({
1791
+ projectId: z8.string()
1792
+ });
1793
+ var ReportTaskAuditResultRequestSchema = z8.object({
1794
+ projectId: z8.string(),
1795
+ taskId: z8.string(),
1796
+ summary: z8.string(),
1797
+ turnGrades: z8.array(
1798
+ z8.object({
1799
+ turnIndex: z8.number(),
1800
+ phase: z8.enum(["planning", "building", "human"]),
1801
+ grade: z8.enum(["correct", "neutral", "blunder"]),
1802
+ reasoning: z8.string(),
1803
+ eventType: z8.string(),
1804
+ eventSummary: z8.string()
1630
1805
  })
1631
1806
  ),
1632
- planningAccuracy: z7.number().nullable(),
1633
- buildingAccuracy: z7.number().nullable(),
1634
- humanAccuracy: z7.number().nullable(),
1635
- planningCorrect: z7.number(),
1636
- planningNeutral: z7.number(),
1637
- planningBlunder: z7.number(),
1638
- buildingCorrect: z7.number(),
1639
- buildingNeutral: z7.number(),
1640
- buildingBlunder: z7.number(),
1641
- humanCorrect: z7.number(),
1642
- humanNeutral: z7.number(),
1643
- humanBlunder: z7.number(),
1644
- humanEvaluations: z7.array(
1645
- z7.object({
1646
- messageIndex: z7.number(),
1647
- rating: z7.union([z7.literal(-1), z7.literal(0), z7.literal(1)]),
1648
- reasoning: z7.string()
1807
+ planningAccuracy: z8.number().nullable(),
1808
+ buildingAccuracy: z8.number().nullable(),
1809
+ humanAccuracy: z8.number().nullable(),
1810
+ planningCorrect: z8.number(),
1811
+ planningNeutral: z8.number(),
1812
+ planningBlunder: z8.number(),
1813
+ buildingCorrect: z8.number(),
1814
+ buildingNeutral: z8.number(),
1815
+ buildingBlunder: z8.number(),
1816
+ humanCorrect: z8.number(),
1817
+ humanNeutral: z8.number(),
1818
+ humanBlunder: z8.number(),
1819
+ humanEvaluations: z8.array(
1820
+ z8.object({
1821
+ messageIndex: z8.number(),
1822
+ rating: z8.union([z8.literal(-1), z8.literal(0), z8.literal(1)]),
1823
+ reasoning: z8.string()
1649
1824
  })
1650
1825
  ).optional(),
1651
- suggestionIds: z7.array(z7.string()),
1652
- auditCostUsd: z7.number().nullable(),
1653
- model: z7.string().nullable(),
1826
+ suggestionIds: z8.array(z8.string()),
1827
+ auditCostUsd: z8.number().nullable(),
1828
+ model: z8.string().nullable(),
1654
1829
  /** When set, the audit is marked failed with this message instead. */
1655
- error: z7.string().optional()
1830
+ error: z8.string().optional()
1656
1831
  });
1657
- var GetTaskAuditsRequestSchema = z7.object({
1658
- projectId: z7.string(),
1659
- limit: z7.number().int().positive().max(200).optional().default(50)
1832
+ var GetTaskAuditsRequestSchema = z8.object({
1833
+ projectId: z8.string(),
1834
+ limit: z8.number().int().positive().max(200).optional().default(50)
1660
1835
  });
1661
- var GetTaskAuditRequestSchema = z7.object({
1662
- projectId: z7.string(),
1663
- auditId: z7.string()
1836
+ var GetTaskAuditRequestSchema = z8.object({
1837
+ projectId: z8.string(),
1838
+ auditId: z8.string()
1664
1839
  });
1665
- var GetTaskAuditAggregatesRequestSchema = z7.object({
1666
- projectId: z7.string()
1840
+ var GetTaskAuditAggregatesRequestSchema = z8.object({
1841
+ projectId: z8.string()
1667
1842
  });
1668
- var DeleteTaskAuditRequestSchema = z7.object({
1669
- projectId: z7.string(),
1670
- auditId: z7.string(),
1671
- requestingUserId: z7.string().optional()
1843
+ var DeleteTaskAuditRequestSchema = z8.object({
1844
+ projectId: z8.string(),
1845
+ auditId: z8.string(),
1846
+ requestingUserId: z8.string().optional()
1672
1847
  });
1673
- var MarkInitialPromptSubmittedRequestSchema = z7.object({
1674
- sessionId: z7.string()
1848
+ var MarkInitialPromptSubmittedRequestSchema = z8.object({
1849
+ sessionId: z8.string()
1675
1850
  });
1676
1851
  var CRITICAL_AUTOMATED_SOURCES = /* @__PURE__ */ new Set([
1677
1852
  "ci_failure",
@@ -1684,6 +1859,52 @@ var CRITICAL_AUTOMATED_SOURCES = /* @__PURE__ */ new Set([
1684
1859
  // reported completed for a prior turn. Only ever sent to parent tasks.
1685
1860
  "parent"
1686
1861
  ]);
1862
+ var PACK_EXECUTIONS = ["single-pod", "fan-out"];
1863
+ var DEFAULT_PACK_EXECUTION = "single-pod";
1864
+ function parsePackExecution(value) {
1865
+ return PACK_EXECUTIONS.includes(value ?? "") ? value : DEFAULT_PACK_EXECUTION;
1866
+ }
1867
+ var MEETING_TRANSCRIPT_MAX_CHARS = 2e6;
1868
+ var MEETING_TITLE_MAX = 200;
1869
+ var CreateMeetingFromTranscriptRequestSchema = z9.object({
1870
+ projectId: z9.string().cuid(),
1871
+ rawText: z9.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
1872
+ title: z9.string().min(1).max(MEETING_TITLE_MAX).optional(),
1873
+ /** ISO 8601. Defaults to now when the source carries no date. */
1874
+ occurredAt: z9.string().datetime().optional(),
1875
+ /** Override auto-detection. Rarely needed; detection handles the three formats. */
1876
+ format: z9.enum(["text", "vtt", "srt"]).optional(),
1877
+ source: z9.enum(["manual", "slack"]).optional()
1878
+ });
1879
+ var GetMeetingRequestSchema = z9.object({
1880
+ projectId: z9.string().cuid(),
1881
+ meetingId: z9.string().cuid()
1882
+ });
1883
+ var UpdateMeetingRequestSchema = z9.object({
1884
+ projectId: z9.string().cuid(),
1885
+ meetingId: z9.string().cuid(),
1886
+ title: z9.string().min(1).max(MEETING_TITLE_MAX).optional(),
1887
+ occurredAt: z9.string().datetime().optional()
1888
+ });
1889
+ var RegenerateMeetingSummaryRequestSchema = z9.object({
1890
+ projectId: z9.string().cuid(),
1891
+ meetingId: z9.string().cuid()
1892
+ });
1893
+ var DeleteMeetingRequestSchema = z9.object({
1894
+ projectId: z9.string().cuid(),
1895
+ meetingId: z9.string().cuid()
1896
+ });
1897
+ var ListMeetingsRequestSchema = z9.object({
1898
+ projectId: z9.string().cuid(),
1899
+ limit: z9.number().int().min(1).max(50).optional(),
1900
+ search: z9.string().max(200).optional()
1901
+ });
1902
+ var ReadMeetingTranscriptRequestSchema = z9.object({
1903
+ projectId: z9.string().cuid(),
1904
+ meetingId: z9.string().cuid(),
1905
+ offset: z9.number().int().min(0).optional(),
1906
+ limit: z9.number().int().min(1).max(500).optional()
1907
+ });
1687
1908
  var AGENT_STATUS_REASON_USER_QUESTION = "user_question";
1688
1909
  var TASK_CHAT_HISTORY_LIMIT = 20;
1689
1910
  var PM_CHAT_HISTORY_LIMIT = 40;
@@ -2034,6 +2255,12 @@ var surfaceTypes = (surface) => Object.keys(CARD_TYPE_SURFACE).filter(
2034
2255
  var BOARD_CARD_TYPES = surfaceTypes("board");
2035
2256
  var REPORT_CARD_TYPES = surfaceTypes("report");
2036
2257
 
2258
+ // src/connection/auth-errors.ts
2259
+ function isPermissionDeniedError(err) {
2260
+ const message = err instanceof Error ? err.message : String(err);
2261
+ return /insufficient permissions|authentication required/i.test(message);
2262
+ }
2263
+
2037
2264
  // src/runner/mode-controller.ts
2038
2265
  var ModeController = class {
2039
2266
  _mode;
@@ -2041,10 +2268,46 @@ var ModeController = class {
2041
2268
  _pendingModeRestart = false;
2042
2269
  _runnerMode;
2043
2270
  _isAuto;
2271
+ /**
2272
+ * Which planner this is. Only read when `_runnerMode` is `plan`; see
2273
+ * `effectiveMode` for the full reasoning, and `latchPlanFlavor` for why this
2274
+ * latches toward `chat` and never away from it.
2275
+ */
2276
+ _planFlavor = "discovery";
2044
2277
  constructor(initialMode, runnerMode = "task", isAuto = false) {
2045
2278
  this._mode = initialMode;
2046
2279
  this._runnerMode = runnerMode;
2047
2280
  this._isAuto = isAuto;
2281
+ this.latchPlanFlavor(initialMode);
2282
+ }
2283
+ /**
2284
+ * Promote this planner to the chat flavor, once, and never demote it.
2285
+ *
2286
+ * It cannot be a constructor-only assignment, because the constructor usually
2287
+ * has nothing to go on. A GKE pod's bootstrap bundle carries no agentMode, so
2288
+ * `CONVEYOR_AGENT_MODE` is unset and `initialMode` falls back to `"building"`
2289
+ * for EVERY pod — which is the whole reason `applyServerMode` exists. Pinning
2290
+ * the flavor at construction alone would therefore make every chat card in the
2291
+ * cloud a discovery planner and silently re-open the defect this replaced.
2292
+ *
2293
+ * One-way, and both directions matter:
2294
+ *
2295
+ * - Only the literal card mode `chat` promotes. The server reports `chat` only
2296
+ * for a card whose agentMode IS `chat`, so a discovery card can never be
2297
+ * talked into the build-capable flavor. The original hazard — `auto` stamped
2298
+ * onto a plan-less auto card's planner — is untouched, because `auto` is not
2299
+ * `chat`.
2300
+ * - Nothing demotes. The Build press overwrites `Task.agentMode` to
2301
+ * `auto`/`building` before the server sees it, so a later stamp would
2302
+ * otherwise strip a live conversation's prompt and file deliverables
2303
+ * mid-turn.
2304
+ *
2305
+ * The asymmetry with `auto` is principled rather than convenient: an auto
2306
+ * card's mode and its planner phase legitimately differ, while a chat card's
2307
+ * mode IS what its session is, for the session's whole life.
2308
+ */
2309
+ latchPlanFlavor(agentMode) {
2310
+ if (agentMode === "chat") this._planFlavor = "chat";
2048
2311
  }
2049
2312
  // ── Getters ────────────────────────────────────────────────────────
2050
2313
  get mode() {
@@ -2065,8 +2328,28 @@ var ModeController = class {
2065
2328
  set pendingModeRestart(val) {
2066
2329
  this._pendingModeRestart = val;
2067
2330
  }
2068
- /** Effective mode accounting for PM/task defaults */
2331
+ /**
2332
+ * Effective mode accounting for PM/task defaults.
2333
+ *
2334
+ * A `plan` runner is pinned to ONE mode for its whole life, whatever the
2335
+ * card's own agentMode later says. That pin is what makes the planner
2336
+ * read-only, non-build-capable, and prompted for planning — all three derive
2337
+ * from here — and it has to be immune to `applyServerMode`, which runs AFTER
2338
+ * boot and would otherwise stamp `auto` onto the planner of a plan-less auto
2339
+ * card, handing it the build prompt and `--dangerously-skip-permissions`
2340
+ * inside a session whose whole purpose is not to have them.
2341
+ *
2342
+ * There are TWO planner flavors, and which one this session is was decided at
2343
+ * boot (`_planFlavor`) — never re-read from the mutable `_mode`. A chat card
2344
+ * boots a plan session too (it is a Planner-session flavor, not a builder in
2345
+ * disguise), and it must keep `buildChatPrompt`, its file deliverables, and
2346
+ * the build-capable tool handler. Reading the flavor from `_mode` would put
2347
+ * that choice back under server control and re-open the exact hole the
2348
+ * paragraph above closes, in the other direction: a stamp of `chat` onto a
2349
+ * discovery planner would hand it build capability.
2350
+ */
2069
2351
  get effectiveMode() {
2352
+ if (this._runnerMode === "plan") return this._planFlavor;
2070
2353
  if (this._mode) return this._mode;
2071
2354
  if (this._runnerMode === "pm") {
2072
2355
  return this._isAuto ? "auto" : "discovery";
@@ -2092,6 +2375,7 @@ var ModeController = class {
2092
2375
  * still get the correct mode.
2093
2376
  */
2094
2377
  applyServerMode(agentMode, isAuto) {
2378
+ this.latchPlanFlavor(agentMode);
2095
2379
  if (agentMode) {
2096
2380
  this._mode = agentMode;
2097
2381
  this._isAuto = agentMode === "auto" || !!isAuto;
@@ -2112,6 +2396,7 @@ var ModeController = class {
2112
2396
  this._mode = "review";
2113
2397
  return this._mode;
2114
2398
  }
2399
+ if (this._runnerMode === "plan") return this.effectiveMode;
2115
2400
  if (this._mode === "auto" && this.canBypassPlanning(context)) {
2116
2401
  this.transitionToBuilding(context);
2117
2402
  return this._mode;
@@ -2203,7 +2488,7 @@ function defineTool(name, description, schema, handler, options) {
2203
2488
 
2204
2489
  // src/harness/claude-code/index.ts
2205
2490
  import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
2206
- import { z as z8 } from "zod";
2491
+ import { z as z10 } from "zod";
2207
2492
  var ClaudeCodeHarness = class {
2208
2493
  /** The SDK stream is itself the structured-event source. */
2209
2494
  emitsStructuredEvents = true;
@@ -2236,7 +2521,7 @@ var ClaudeCodeHarness = class {
2236
2521
  }
2237
2522
  );
2238
2523
  if (t.strict) {
2239
- sdkTool.inputSchema = z8.strictObject(t.schema);
2524
+ sdkTool.inputSchema = z10.strictObject(t.schema);
2240
2525
  }
2241
2526
  return sdkTool;
2242
2527
  });
@@ -3494,7 +3779,7 @@ var PtyOutputCoalescer = class {
3494
3779
 
3495
3780
  // src/harness/pty/tool-server.ts
3496
3781
  import { createServer as createServer2 } from "http";
3497
- import { z as z9 } from "zod";
3782
+ import { z as z11 } from "zod";
3498
3783
  import { writeFile as writeFile3 } from "fs/promises";
3499
3784
  import { join as join3 } from "path";
3500
3785
  import { randomBytes } from "crypto";
@@ -3551,7 +3836,7 @@ var PtyToolServer = class {
3551
3836
  const mcp = new McpServer({ name: this.name, version: "1.0.0" });
3552
3837
  const register = mcp.registerTool.bind(mcp);
3553
3838
  for (const tool2 of this.tools) {
3554
- const inputSchema = tool2.strict ? z9.strictObject(tool2.schema) : tool2.schema;
3839
+ const inputSchema = tool2.strict ? z11.strictObject(tool2.schema) : tool2.schema;
3555
3840
  register(
3556
3841
  tool2.name,
3557
3842
  {
@@ -6427,13 +6712,13 @@ Respond to them with post_to_chat (your turn output is NOT shown in chat).`
6427
6712
  if (!context.plan?.trim()) {
6428
6713
  parts.push(
6429
6714
  `
6430
- If this conversation turns into a development task: save a plan with update_task_plan first \u2014 that identifies the card and moves it to In Progress \u2014 then implement it and open a PR with mcp__conveyor__create_pull_request, like a normal build.`
6715
+ If this conversation turns into a development task: save a plan with update_task first \u2014 that identifies the card and moves it to In Progress \u2014 then implement it and open a PR with mcp__conveyor__create_pull_request, like a normal build.`
6431
6716
  );
6432
6717
  return parts;
6433
6718
  }
6434
6719
  parts.push(
6435
6720
  `
6436
- This card has a saved plan, so it is a development task now. Implement the plan on the git branch "${context.githubBranch}", verify with \`bun run check\` and \`bun run test:affected\`, then open the PR with mcp__conveyor__create_pull_request.`
6721
+ This card has a saved plan, so it is a development task now. Implement it on the git branch "${context.githubBranch}", then run the \`/conveyor-build\` skill and follow it \u2014 the same workflow any build on this card would use, including how to verify before opening the PR.`
6437
6722
  );
6438
6723
  if (context.githubPRUrl) {
6439
6724
  parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
@@ -6569,7 +6854,7 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
6569
6854
  ``,
6570
6855
  `1. Call list_subtasks to see the current state of all child tasks.`,
6571
6856
  ` The response includes PR info, agent assignment, **dependency info** (dependsOn array + allDependenciesMet flag), and the **packSlots** build-slot picture.`,
6572
- ` If list_subtasks returns NO children, this is a fresh parent card: break the work down now \u2014 explore the codebase, save a parent-level plan with update_task_plan, then create child tasks with create_subtask, each with a detailed plan (file:line citations, verification steps) and dependsOn set for any child that blocks on another. Then fire the ready children and continue the loop.`,
6857
+ ` If list_subtasks returns NO children, this is a fresh parent card: break the work down now \u2014 explore the codebase, save a parent-level plan with update_task, then create child tasks with create_subtask, each with a detailed plan (file:line citations, verification steps) and dependsOn set for any child that blocks on another. Then fire the ready children and continue the loop.`,
6573
6858
  ``,
6574
6859
  `2. Evaluate children by status and dependency readiness:`,
6575
6860
  ` - "ReviewPR": Review and merge its PR with approve_and_merge_pr. (Highest priority)`,
@@ -6622,6 +6907,73 @@ function buildPackRunnerSystemPrompt(context, config, setupLog) {
6622
6907
  parts.push(...PACK_RUNNER_FOOTER);
6623
6908
  return parts.join("\n");
6624
6909
  }
6910
+ function buildSinglePodPackPrompt(context, config, setupLog) {
6911
+ const packBranch = resolveMergedWorkBranch(context);
6912
+ const parts = [
6913
+ `You are an autonomous Pack Runner for the "${context.title}" pack, and you implement the work yourself.`,
6914
+ ``,
6915
+ `Run the \`/conveyor-build\` skill and follow its PACK PATH. It is the source of truth for the loop \u2014 child selection, dependency order, the reviewer-of-record pass, the finale \u2014 and it carries the pod substitution this session runs under. Do not re-derive the workflow from memory.`,
6916
+ ``,
6917
+ // Session-bound facts only. Everything the skill CAN know now lives there;
6918
+ // what stays is what is true of THIS pod and this card.
6919
+ `## This session`,
6920
+ `- You are ALREADY bound to this parent card. There is nothing to claim.`,
6921
+ `- One checkout, one branch: \`${packBranch}\`. Every child's work is commits on it.`,
6922
+ `- Children get NO branches and NO pull requests here. \`create_pull_request\` always opens the PR for the card you are bound to, so calling it mid-pack opens the PARENT's PR early and strands the remaining children. One PR, at the very end, for this parent.`,
6923
+ `- You move each child's status yourself: \`update_task(task_id: <child>, status: ...)\`. No per-child build fires, so a status you do not write is a board that silently lies. A child is "ReviewDev" once its commits are on \`${packBranch}\`.`,
6924
+ `- \`start_child_cloud_build\` and \`stop_child_build\` belong to the fan-out model and are not available to you \u2014 you are the implementer.`,
6925
+ `- Merge \`origin/dev\` into \`${packBranch}\` after each child, never rebase: the branch is shared with WIP refs and rewriting it breaks them.`,
6926
+ `- Nothing will wake you to start the next child. Go idle only when genuinely blocked on something external.`,
6927
+ ``
6928
+ ];
6929
+ if (context.storyPoints && context.storyPoints.length > 0) {
6930
+ parts.push(...formatStoryPoints(context.storyPoints));
6931
+ }
6932
+ if (context.agents && context.agents.length > 0) {
6933
+ parts.push(...formatProjectAgents(context.agents));
6934
+ }
6935
+ if (setupLog.length > 0) {
6936
+ parts.push(``, `## Environment setup log`, "```", ...setupLog, "```");
6937
+ }
6938
+ if (context.agentInstructions) {
6939
+ parts.push(``, `## Agent Instructions`, context.agentInstructions);
6940
+ }
6941
+ if (config.instructions) {
6942
+ parts.push(``, `## Additional Instructions`, config.instructions);
6943
+ }
6944
+ parts.push(...PACK_RUNNER_FOOTER);
6945
+ return parts.join("\n");
6946
+ }
6947
+ function buildSinglePodPackInstructions(context, scenario) {
6948
+ const parts = [`
6949
+ ## Instructions`];
6950
+ if (scenario === "fresh") {
6951
+ parts.push(
6952
+ `You are the Pack Runner for this task and its subtasks, and you implement them yourself.`,
6953
+ `Start now: call list_subtasks to see where the pack stands.`,
6954
+ `No children yet? Break the work down first \u2014 save a parent-level plan with update_task, then create_subtask each child with a detailed plan and dependsOn set where one blocks another.`,
6955
+ `Otherwise take the first "Open" child whose dependencies are met, claim it with update_task(task_id, status: "InProgress"), and implement it on this branch.`
6956
+ );
6957
+ } else if (scenario === "idle_relaunch") {
6958
+ parts.push(
6959
+ `You have been relaunched. Re-derive where the pack stands \u2014 call list_subtasks and check the branch, never rely on what you remember.`,
6960
+ `A child left "InProgress" is one YOU were implementing: check the working tree and the log for what already landed before redoing any of it.`,
6961
+ `Otherwise continue the loop: next ready child, or the finale if none remain.`
6962
+ );
6963
+ } else {
6964
+ const lastAgentIdx = findLastAgentMessageIndex2(context.chatHistory);
6965
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
6966
+ parts.push(
6967
+ `You have been relaunched with new messages.`,
6968
+ `
6969
+ New messages since your last run:`,
6970
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
6971
+ `
6972
+ After addressing the feedback, resume: call list_subtasks and continue with the next ready child.`
6973
+ );
6974
+ }
6975
+ return parts;
6976
+ }
6625
6977
  function buildPackRunnerInstructions(context, scenario) {
6626
6978
  const parts = [`
6627
6979
  ## Instructions`];
@@ -6629,7 +6981,7 @@ function buildPackRunnerInstructions(context, scenario) {
6629
6981
  parts.push(
6630
6982
  `You are the Pack Runner for this task and its subtasks.`,
6631
6983
  `Begin your autonomous loop immediately: call list_subtasks to assess the current state.`,
6632
- `If there are no children yet, create them first \u2014 save a parent-level plan with update_task_plan, then create child tasks with detailed plans and dependsOn set where one blocks on another. Then fire the ready ones.`,
6984
+ `If there are no children yet, create them first \u2014 save a parent-level plan with update_task, then create child tasks with detailed plans and dependsOn set where one blocks on another. Then fire the ready ones.`,
6633
6985
  `If any child is in "ReviewPR" status, review and merge its PR first.`,
6634
6986
  `Then fire the next "Open" child task.`
6635
6987
  );
@@ -6693,39 +7045,6 @@ function baseDiffCommand(baseBranch, flags) {
6693
7045
  const suffix = flags ? ` ${flags}` : "";
6694
7046
  return `git fetch origin ${base} -q && git diff $(git merge-base origin/${base} HEAD)..HEAD${suffix}`;
6695
7047
  }
6696
- function gateFailureModes() {
6697
- return [
6698
- `Reading a gate result correctly is what keeps this to ONE pass:`,
6699
- `- Capture AND propagate the exit code (\`<gate> > <log> 2>&1; ec=$?; echo "EXIT:$ec" >> <log>; (exit $ec)\`) \u2014 piping a gate through \`tail\` masks it, and a bare trailing \`echo "EXIT:$?"\` makes the wrapper exit 0 over a failed gate. That \`EXIT:\` line, plus turbo's final \`Tasks:\` count, is the authority on pass/fail.`,
6700
- `- A green gate that printed no per-suite summary (no \`Test Files\`/\`Tests\` line) is still green: \`turbo.json\` sets \`outputLogs: "errors-only"\`, so a PASSING run prints nothing per suite. Do NOT re-run a clean-exit gate to "see the counts" \u2014 if you genuinely need them, run one package's script directly (\`bun run --cwd <pkg> test\`).`,
6701
- `- Exit 143, or \`singleton: stopping running '<label>'\` on stderr, means a second gate you started evicted this one (on a pod the heavy gates share one lock). 143 is never a pass \u2014 but an evicted run never finished, so re-running it alone is still your one pass, not a repeat. Run one gate at a time; \`bun run check\` is not lock-wrapped, so it is safe alongside a test run.`,
6702
- `- Confirm every package your diff touches actually appears in the run. \`--affected\` can select nothing for a package you changed; when that happens run just that package's suite directly, e.g. \`bun run --cwd apps/api test:unit <changed test files>\` \u2014 not the whole gate again.`,
6703
- ...gateWaitProtocol()
6704
- ];
6705
- }
6706
- function gateWaitProtocol() {
6707
- return [
6708
- `How to wait for a gate \u2014 one gate, one wait, no polling:`,
6709
- `- Launch it ONCE with \`run_in_background: true\`, then END YOUR TURN. The completion notification re-invokes you. Do not read the output file, \`tail\`/\`wc\`/\`pgrep\` it, or start a second watcher on a log another run already owns \u2014 a quiet gate is still running.`,
6710
- `- Never wrap a gate in a foreground \`timeout\`: killing your own run at 590s produces exit 124/143 and ZERO information, and you then have to run it again unbounded. \`sleep N; <cmd>\` is hard-blocked for the same reason.`,
6711
- `- Never relaunch a command you just killed without changing something. State cwd explicitly in the command (\`cd /workspaces/repo && \u2026\`, \`--root\`, \`git -C\`) rather than trusting the shell's working directory.`,
6712
- `- The completion notification reports the WRAPPER's exit code. With the propagating idiom above, \`(exit $ec)\` makes that the gate's own code; a wrapper that ends on a bare \`echo\` reports 0 no matter what failed. Whenever the notification and the log disagree, the \`EXIT:\` line in the log is the authority.`,
6713
- `- Scope heavy gates to what you changed (\`--filter=<pkg>\`). An unscoped \`check:affected\` can pull a large web typecheck into scope for a diff touching no web files and get OOM-killed (exit 137) after minutes, where the scoped run takes seconds.`,
6714
- `- Merge the base BEFORE the gate pass, never after (see the Pre-PR Protocol). If that merge touched \`package.json\`/\`bun.lock\`, run \`bun install\` before starting the gate \u2014 a changed lockfile makes gates fail for reasons unrelated to your diff.`
6715
- ];
6716
- }
6717
- function ciTriageChecklist(baseBranch) {
6718
- const base = baseBranch?.trim() || "dev";
6719
- return [
6720
- `Before treating a CI failure as yours, run these four checks first \u2014 most reported reds are not caused by the diff:`,
6721
- `1. Is the failing run's head SHA still current? A red run against a superseded commit is stale.`,
6722
- `2. Is the only \`##[error]\` a cancellation ("The operation was canceled", "The runner has received a shutdown signal")? That is a reclaimed/preempted runner, not a code failure \u2014 re-run it.`,
6723
- `3. Is the failing file even in your diff? \`git diff origin/${base}...HEAD --stat -- <path>\` \u2014 empty output means you did not touch it. Check whether \`origin/${base}\` already fails the same way before investigating further.`,
6724
- `4. Is \`origin/${base}\` itself green? A semantic merge conflict on the base branch fails every open PR at once.`,
6725
- `Known environmental failures \u2014 recognize, do not re-diagnose: suites needing a real Elasticsearch fail without \`RC_TEST_ES_URL\` and cannot pass in a pod; exit 137 is a pod OOM, not a code error; \`gh\` returning \`HTTP 401: Bad credentials\` means the pod token aged out (~1h) \u2014 re-exporting it does not help, so use the Conveyor MCP CI-status tools instead of spending calls re-testing \`gh\`.`,
6726
- `A Dependabot PR showing "no checks reported" is in \`action_required\` \u2014 a maintainer must approve the workflow run. That is not "CI did not run", and you cannot unblock it yourself.`
6727
- ];
6728
- }
6729
7048
  function formatFileSize(bytes) {
6730
7049
  if (bytes === void 0) return "";
6731
7050
  if (bytes < 1024) return `${bytes}B`;
@@ -6772,8 +7091,11 @@ function formatTaskFile(file) {
6772
7091
  }
6773
7092
  function formatChatHistory(chatHistory, limit) {
6774
7093
  const relevant = chatHistory.slice(-(limit ?? PM_CHAT_HISTORY_LIMIT));
6775
- const parts = [`
6776
- ## Recent Chat Context`];
7094
+ const parts = [
7095
+ `
7096
+ ## New Messages`,
7097
+ `Posted since this card's last agent turn. For the rest of the conversation call \`mcp__conveyor__read_task_chat\`.`
7098
+ ];
6777
7099
  for (const msg of relevant) {
6778
7100
  const sender = msg.userName ?? msg.role;
6779
7101
  parts.push(`[${sender}]: ${msg.content}`);
@@ -6852,106 +7174,371 @@ function formatIncidents(incidents) {
6852
7174
  return parts;
6853
7175
  }
6854
7176
 
6855
- // src/execution/tag-context-resolver.ts
6856
- var TYPE_PRIORITY = { rule: 0, doc: 1, file: 2, folder: 3 };
6857
- var SUMMARY_SCAN_CHARS = 4e3;
6858
- var SUMMARY_MAX_CHARS = 160;
6859
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
6860
- ".png",
6861
- ".jpg",
6862
- ".jpeg",
6863
- ".gif",
6864
- ".webp",
6865
- ".ico",
6866
- ".svg",
6867
- ".bmp",
6868
- ".mp3",
6869
- ".mp4",
6870
- ".wav",
6871
- ".avi",
6872
- ".mov",
6873
- ".pdf",
6874
- ".zip",
6875
- ".tar",
6876
- ".gz",
6877
- ".woff",
6878
- ".woff2",
6879
- ".ttf",
6880
- ".eot",
6881
- ".otf",
6882
- ".exe",
6883
- ".dll",
6884
- ".so",
6885
- ".dylib",
6886
- ".wasm"
6887
- ]);
6888
- function isBinaryPath(filePath) {
6889
- const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
6890
- return BINARY_EXTENSIONS.has(ext);
7177
+ // src/execution/mode-prompt.ts
7178
+ var SP_DESC_MAX_CHARS = 80;
7179
+ function truncateDescription(desc, maxChars) {
7180
+ if (desc.length <= maxChars) return desc;
7181
+ return desc.slice(0, maxChars) + "\u2026";
6891
7182
  }
6892
- var fileSummaryCache = /* @__PURE__ */ new Map();
6893
- var folderListingCache = /* @__PURE__ */ new Map();
6894
- var fileReadCount = 0;
6895
- var folderReadCount = 0;
6896
- function deriveSummary(raw) {
6897
- let body = raw;
6898
- let frontmatter = "";
6899
- if (raw.startsWith("---")) {
6900
- const end = raw.indexOf("\n---", 3);
6901
- if (end !== -1) {
6902
- frontmatter = raw.slice(3, end);
6903
- const afterClose = raw.indexOf("\n", end + 1);
6904
- body = afterClose === -1 ? "" : raw.slice(afterClose + 1);
6905
- }
6906
- }
6907
- const fmDescription = frontmatter.split("\n").map((l) => l.trim()).find((l) => /^(description|title):/i.test(l));
6908
- if (fmDescription) {
6909
- const value = fmDescription.slice(fmDescription.indexOf(":") + 1).trim().replace(/^["']|["']$/g, "");
6910
- if (value) return truncateSummary(value);
6911
- }
6912
- for (const rawLine of body.split("\n")) {
6913
- const line = rawLine.trim();
6914
- if (!line) continue;
6915
- const cleaned = line.replace(/^#+\s*/, "").replace(/^[-*]\s+/, "").trim();
6916
- if (cleaned) return truncateSummary(cleaned);
7183
+ function formatTagWithContextPaths(tag) {
7184
+ const desc = tag.description ? ` \u2014 ${tag.description}` : "";
7185
+ const lines = [`- Name: "${tag.name}"${desc}`];
7186
+ for (const link of tag.contextPaths ?? []) {
7187
+ const label = link.label ? ` (${link.label})` : "";
7188
+ lines.push(` \u2192 ${link.type}: ${link.path}${label}`);
6917
7189
  }
6918
- return null;
7190
+ return lines;
6919
7191
  }
6920
- function truncateSummary(text) {
6921
- const collapsed = text.replace(/\s+/g, " ").trim();
6922
- return collapsed.length > SUMMARY_MAX_CHARS ? collapsed.slice(0, SUMMARY_MAX_CHARS - 1).trimEnd() + "\u2026" : collapsed;
7192
+ function buildEstimateReassessmentLines(context) {
7193
+ const currentSp = context.taskStoryPointValue ?? "unset";
7194
+ const currentRisk = context.taskRiskLevel ?? "unset";
7195
+ return [
7196
+ `Story points and risk are LIVING estimates, not one-time labels. Reassess them at every phase \u2014 planning, building, review \u2014 against your current understanding, and adjust in EITHER direction: work that looks big early is often small once scoped, and vice versa. Lowering an inflated estimate is as valuable as raising an underestimate; a stale value is worse than a changed one.`,
7197
+ `Current values: story points ${currentSp}, risk ${currentRisk}.`,
7198
+ ``,
7199
+ `Risk levels (how much important surface the change touches):`,
7200
+ `- critical: foundational surface \u2014 auth, billing, data integrity, migrations`,
7201
+ `- high: important surface with broad blast radius`,
7202
+ `- medium: moderate, contained surface area`,
7203
+ `- low: small or isolated change`
7204
+ ];
6923
7205
  }
6924
- async function readFileSummary(filePath) {
6925
- try {
6926
- if (isBinaryPath(filePath)) return null;
6927
- const st = await statWorkspacePath(filePath);
6928
- if (!st.exists) return null;
6929
- const mtimeMs = st.mtimeMs;
6930
- const cached = fileSummaryCache.get(filePath);
6931
- if (cached && cached.mtimeMs === mtimeMs) {
6932
- return cached.summary;
7206
+ function buildPropertyInstructions(context, runnerMode) {
7207
+ const isTask = runnerMode === "task";
7208
+ const parts = [];
7209
+ parts.push(
7210
+ ``,
7211
+ `### Proactive Property Management`,
7212
+ `As you work this task, proactively keep task properties accurate:`,
7213
+ `- Use update_task_properties to set any combination of: title, story points, risk, and tags`,
7214
+ `- You can update all properties at once or just one at a time as needed`,
7215
+ `- Icons are assigned automatically during identification \u2014 do not set icons manually`,
7216
+ ``,
7217
+ ...buildEstimateReassessmentLines(context),
7218
+ ``,
7219
+ `Don't wait for the user to ask \u2014 keep these accurate as the work takes shape.`,
7220
+ `If scope changes materially at any point, update the properties to match.`
7221
+ );
7222
+ if (context.storyPoints && context.storyPoints.length > 0) {
7223
+ parts.push(``, `Available story point tiers:`);
7224
+ for (const sp of context.storyPoints) {
7225
+ const desc = sp.description ? ` \u2014 ${truncateDescription(sp.description, SP_DESC_MAX_CHARS)}` : "";
7226
+ parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
6933
7227
  }
6934
- const raw = await readWorkspaceFile(filePath);
6935
- fileReadCount++;
6936
- const summary = deriveSummary(raw.slice(0, SUMMARY_SCAN_CHARS));
6937
- fileSummaryCache.set(filePath, { mtimeMs, summary });
6938
- return summary;
6939
- } catch {
6940
- return null;
6941
7228
  }
6942
- }
6943
- async function readFolderListing(folderPath) {
6944
- try {
6945
- const st = await statWorkspacePath(folderPath);
6946
- if (!st.exists) return null;
6947
- const mtimeMs = st.mtimeMs;
6948
- const cached = folderListingCache.get(folderPath);
6949
- if (cached && cached.mtimeMs === mtimeMs) {
6950
- return cached.listing;
7229
+ if (context.projectTags && context.projectTags.length > 0) {
7230
+ const assignedIds = new Set(context.taskTagIds ?? []);
7231
+ const assigned = context.projectTags.filter((t) => assignedIds.has(t.id));
7232
+ const unassigned = context.projectTags.filter((t) => !assignedIds.has(t.id));
7233
+ if (assigned.length > 0) {
7234
+ parts.push(``, `Assigned tags:`);
7235
+ for (const tag of assigned) parts.push(...formatTagWithContextPaths(tag));
6951
7236
  }
6952
- const entries = await readWorkspaceDir(folderPath);
6953
- folderReadCount++;
6954
- const listing = `Files: ${entries.join(", ")}`;
7237
+ if (!isTask && unassigned.length > 0) {
7238
+ parts.push(``, `Available project tags:`);
7239
+ for (const tag of unassigned) parts.push(...formatTagWithContextPaths(tag));
7240
+ }
7241
+ parts.push(
7242
+ ``,
7243
+ `Tags are the project glossary. call get_tag("<name>") for a term's full spec (overview, linked files, hierarchy) whenever a tag is assigned to this card or mentioned (@[tag:id]) in chat/plans. To mention a tag yourself, write @[tag:<name>] with the tag's exact name \u2014 the server rewrites it to the id token when it stores the text, so you never need the id. An unknown name is left as plain text. When your work changes how a tagged system behaves, update that tag's overview via update_tag with a short reason \u2014 your card is stamped into the revision history.`
7244
+ );
7245
+ }
7246
+ return parts;
7247
+ }
7248
+ function buildPlanRevisionSection() {
7249
+ return [
7250
+ ``,
7251
+ `### The plan can change while you sleep`,
7252
+ `If a turn opens with "The plan changed since this build was dispatched", honor it BEFORE your next Write or Edit:`,
7253
+ `1. Call get_current_plan and read the current plan in full.`,
7254
+ `2. Compare it against the plan you were launched with \u2014 the current plan wins.`,
7255
+ `3. Drop or redo whatever the revision supersedes, and say so with post_to_chat if it invalidates work you already committed.`,
7256
+ `Building on a superseded plan is the most expensive mistake a resumed card can make.`
7257
+ ];
7258
+ }
7259
+ function buildSkillInvocation(skill) {
7260
+ return [
7261
+ `Run the \`${skill}\` skill and follow it. It is the source of truth for how this work is done \u2014 do not re-derive the workflow from memory.`,
7262
+ `Two things it cannot know, because they are true of this session rather than of the workflow:`,
7263
+ `- You are ALREADY bound to this card. There is nothing to resolve or claim, and no card argument to ask for \u2014 the task context above is it.`,
7264
+ `- Your plan belongs on the card (\`update_task\`), and your PR goes through \`create_pull_request\`. Do not hand-edit card status to fake a transition.`
7265
+ ];
7266
+ }
7267
+ function buildEnforcedToolContracts() {
7268
+ return [
7269
+ ``,
7270
+ `### Contracts this environment enforces`,
7271
+ `- Full coding access (read, write, edit, bash, git). Destructive operations are BLOCKED at the tool layer: use \`--force-with-lease\`, never \`--force\`; no \`git reset --hard\`, no \`rm -rf /\`.`,
7272
+ `- A tool error or "MCP server unavailable" is almost always a transient socket reconnect: RETRY the same call. Do not stop work, go idle, or treat it as a reason to end your turn.`,
7273
+ `- Repeating an identical call is interrupted every 4th time. That is a loop breaker, not a denial of the work \u2014 change something about the call rather than repeating it.`
7274
+ ];
7275
+ }
7276
+ function buildHarnessContracts(context) {
7277
+ return [
7278
+ ...buildEnforcedToolContracts(),
7279
+ ...buildPlanRevisionSection(),
7280
+ ...context?.baseBranch ? [
7281
+ ``,
7282
+ `This card's base branch is \`${context.baseBranch}\` \u2014 pass it explicitly when you open the PR.`
7283
+ ] : []
7284
+ ];
7285
+ }
7286
+ function buildDiscoveryPrompt(context, runnerMode) {
7287
+ const parts = [
7288
+ `
7289
+ ## Mode: Discovery`,
7290
+ `You are in Discovery mode \u2014 planning and scoping this card WITH the team.`,
7291
+ ...buildSkillInvocation("/conveyor-plan"),
7292
+ ...context?.isParentTask ? [
7293
+ `- This card is a pack parent: the deliverable is child cards with real plans and \`dependsOn\` edges, not an implementation plan for yourself.`
7294
+ ] : [],
7295
+ ``,
7296
+ `### Contracts this environment enforces`,
7297
+ `- 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).`,
7298
+ `- A plan file on disk is NOT the plan. Only \`update_task\` saves it to the card.`,
7299
+ `- **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.`,
7300
+ `- ExitPlanMode does NOT start building. It parks this session for human review; the team decides when to build.`,
7301
+ ...runnerMode === "plan" ? [
7302
+ `- You are the Planner half of this card's lifecycle. When the team presses Build, a separate **Builder** session starts on THIS pod and takes over \u2014 you never become it. So the plan has to stand on its own: the Builder inherits this repo checkout, but none of your reasoning.`
7303
+ ] : [],
7304
+ `- A tool error or "MCP server unavailable" is almost always a transient socket reconnect: RETRY the same call rather than ending your turn.`
7305
+ ];
7306
+ if (context) parts.push(...buildPropertyInstructions(context, runnerMode));
7307
+ return parts.join("\n");
7308
+ }
7309
+ function buildAutoPrompt(context, runnerMode) {
7310
+ const parts = [
7311
+ `
7312
+ ## Mode: Auto`,
7313
+ `You are in Auto mode \u2014 plan this card, then build it, without stopping for approval.`,
7314
+ `Run \`/conveyor-plan\` FIRST to produce the plan and save it to the card, then run \`/conveyor-build\` to implement it. Both are sources of truth; do not re-derive them.`,
7315
+ ...buildSkillInvocation("/conveyor-build"),
7316
+ ``,
7317
+ `### What "auto" changes`,
7318
+ `- There is no plan-approval step and no read-only phase. The plan is a RECORD for the team, not a gate \u2014 save it, then immediately write the code. A card whose only output is a plan (or a plan-only PR) has not been built.`,
7319
+ `- Decide independently. Escalate only when genuinely blocked: ambiguous requirements, missing access, conflicting instructions. Everything else is yours to call.`,
7320
+ `- Skip \`/conveyor-plan\` only if the card already carries a plan you are not materially diverging from.`,
7321
+ ...buildHarnessContracts(context)
7322
+ ];
7323
+ if (context) parts.push(...buildPropertyInstructions(context, runnerMode));
7324
+ return parts.join("\n");
7325
+ }
7326
+ function buildBuildingPrompt(context) {
7327
+ const parts = [
7328
+ `
7329
+ ## Mode: Building`,
7330
+ `You are in Building mode \u2014 executing this card's plan.`,
7331
+ ...buildSkillInvocation("/conveyor-build"),
7332
+ ...context?.isParentTask ? [`- This card is a pack parent. \`/conveyor-build\` routes to its pack path; follow that.`] : [],
7333
+ ...buildHarnessContracts(context)
7334
+ ];
7335
+ if (context) parts.push(...buildPropertyInstructions(context));
7336
+ return parts.join("\n");
7337
+ }
7338
+ function buildModePrompt(agentMode, context, runnerMode) {
7339
+ switch (agentMode) {
7340
+ case "discovery":
7341
+ return buildDiscoveryPrompt(context, runnerMode);
7342
+ case "building":
7343
+ return buildBuildingPrompt(context);
7344
+ case "review":
7345
+ return buildReviewPrompt(context);
7346
+ case "auto":
7347
+ return buildAutoPrompt(context, runnerMode);
7348
+ case "chat":
7349
+ return buildChatPrompt(context);
7350
+ default:
7351
+ return null;
7352
+ }
7353
+ }
7354
+ function buildChatPrompt(context) {
7355
+ const base = context?.baseBranch?.trim() || "dev";
7356
+ return [
7357
+ `
7358
+ ## Mode: Chat`,
7359
+ `You are in Chat mode \u2014 a conversational assistant working directly with the user on this card.`,
7360
+ `- Respond conversationally to the user in chat. Ask clarifying questions when useful; this is a back-and-forth, not an autonomous build.`,
7361
+ `- You have full read/write access to the workspace and can run non-destructive shell commands, so you CAN create files (notes, scripts, docs, data, diagrams, etc.) when they help the user.`,
7362
+ `- This card starts Unidentified. That is correct \u2014 it stays a conversation until the work becomes a development task (see below).`,
7363
+ ``,
7364
+ `### Deliverables \u2014 attach files to the card`,
7365
+ `- When you create a file the user should keep, attach it to the card with the \`upload_attachment\` tool so it shows up on the card. Mention in chat what you attached.`,
7366
+ `- \`upload_attachment\` accepts any file type (markdown, PDF, HTML, text, data, images) up to 25MB \u2014 this is how docs are delivered. Do NOT publish deliverables as a Claude artifact or any other off-platform link; the card is the system of record.`,
7367
+ `- For conversational work, attachments are the deliverable. Do NOT open a PR to hand over a document or an answer.`,
7368
+ ``,
7369
+ `### Turning into a development task`,
7370
+ `- If the conversation becomes a request to change the code, escalate the card first: save a plan with \`update_task\`. That first plan identifies the card and moves it to In Progress.`,
7371
+ `- The plan is also the gate on your PR path: \`create_pull_request\`, \`git push\` and \`gh pr\` are DENIED until a plan is saved, and allowed afterwards. Do not try to work around a denial \u2014 save the plan.`,
7372
+ `- Once the plan is saved, it IS a normal build: run the \`/conveyor-build\` skill and follow it, including how to sync \`${base}\` and verify before opening the PR. Do not re-derive that workflow here.`,
7373
+ `- Keep talking to the user while you build \u2014 this is still their card.`,
7374
+ ``,
7375
+ `### Finishing the conversation`,
7376
+ `- When the user indicates they are done (or explicitly asks to wrap up / close the card), call \`force_update_task_status\` with status \`"Complete"\`. It works from either status, so a card still sitting Unidentified can be completed the same way as one that reached In Progress.`,
7377
+ `- If you opened a PR, do NOT complete the card \u2014 the PR review flow takes it from there.`,
7378
+ `- If the user is still engaged, keep helping \u2014 only complete the card once the interaction has concluded.`,
7379
+ `- Do not complete the card while you still owe the user a response or an attachment.`
7380
+ ].join("\n");
7381
+ }
7382
+ function buildReviewTagSection(context) {
7383
+ const assignedIds = new Set(context?.taskTagIds ?? []);
7384
+ const assigned = (context?.projectTags ?? []).filter((t) => assignedIds.has(t.id));
7385
+ if (assigned.length === 0) return [];
7386
+ const parts = [
7387
+ `### Card Tags & Domain Context`,
7388
+ `This card carries these project glossary tags:`
7389
+ ];
7390
+ for (const tag of assigned) parts.push(...formatTagWithContextPaths(tag));
7391
+ parts.push(
7392
+ ``,
7393
+ `A tag's rules and overview define the conventions the code under review must follow. Use them when you judge Pattern Consistency:`,
7394
+ `- Read the tag entries that match the files in this diff. The "Reference Guides" section of the task brief lists the same tags with a one-line summary of each linked doc.`,
7395
+ `- Call get_tag("<name>") for a term's full spec (overview, linked files, hierarchy) when the linked docs are not enough.`,
7396
+ `- A change that contradicts a tagged system's documented conventions is a review finding, even when it reads fine on its own.`,
7397
+ `- Skip the tags this diff does not touch \u2014 do not read them all up front.`,
7398
+ `- If the diff changes how a tagged system behaves and the tag's overview is now wrong, say so in your review.`,
7399
+ ``
7400
+ );
7401
+ return parts;
7402
+ }
7403
+ function buildReviewPrompt(context) {
7404
+ if (context?.isParentTask) return buildParentReviewPrompt();
7405
+ return [
7406
+ `
7407
+ ## Mode: Review`,
7408
+ ...buildSkillInvocation("/conveyor-review"),
7409
+ ``,
7410
+ // Everything below is harness-bound: the skill cannot know this session's
7411
+ // resolved branch, and the verdict tool names differ per surface (the skill
7412
+ // documents both pairs; this names the one that exists here).
7413
+ `### This session`,
7414
+ `- The diff under review: \`${baseDiffCommand(context?.baseBranch)}\``,
7415
+ `- Your verdict tools on this pod are \`approve_code_review\` and \`request_code_changes\`. The local conveyor-mcp pair does not exist here.`,
7416
+ ...buildEnforcedToolContracts(),
7417
+ ...buildReviewTagSection(context)
7418
+ ].join("\n");
7419
+ }
7420
+ function buildParentReviewPrompt() {
7421
+ return [
7422
+ `
7423
+ ## Mode: Review`,
7424
+ `### Parent Task Review`,
7425
+ `You are reviewing and coordinating child tasks.`,
7426
+ `- Use \`list_subtasks\` to see current child task state and progress.`,
7427
+ `- For children in ReviewPR status: review their code quality and merge with \`approve_and_merge_pr\`.`,
7428
+ `- For children with failing CI: check with \`get_execution_logs(childTaskId)\` and escalate if stuck.`,
7429
+ `- Fire next child builds with \`start_child_cloud_build\` when ready.`,
7430
+ `- Create follow-up tasks for issues discovered during review.`,
7431
+ ``,
7432
+ `### Coordination Workflow`,
7433
+ `1. Check child task statuses with \`list_subtasks\``,
7434
+ `2. Review completed children \u2014 check PRs, run tests if needed`,
7435
+ `3. Approve and merge passing PRs`,
7436
+ `4. Fire builds for children that are ready`,
7437
+ `5. Create follow-up tasks for anything out of scope`,
7438
+ `6. As children complete, correct their story points with update_subtask (storyPointValue) when the actual work diverged from the estimate \u2014 either direction`
7439
+ ].join("\n");
7440
+ }
7441
+
7442
+ // src/execution/tag-context-resolver.ts
7443
+ var TYPE_PRIORITY = { rule: 0, doc: 1, file: 2, folder: 3 };
7444
+ var SUMMARY_SCAN_CHARS = 4e3;
7445
+ var SUMMARY_MAX_CHARS = 160;
7446
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
7447
+ ".png",
7448
+ ".jpg",
7449
+ ".jpeg",
7450
+ ".gif",
7451
+ ".webp",
7452
+ ".ico",
7453
+ ".svg",
7454
+ ".bmp",
7455
+ ".mp3",
7456
+ ".mp4",
7457
+ ".wav",
7458
+ ".avi",
7459
+ ".mov",
7460
+ ".pdf",
7461
+ ".zip",
7462
+ ".tar",
7463
+ ".gz",
7464
+ ".woff",
7465
+ ".woff2",
7466
+ ".ttf",
7467
+ ".eot",
7468
+ ".otf",
7469
+ ".exe",
7470
+ ".dll",
7471
+ ".so",
7472
+ ".dylib",
7473
+ ".wasm"
7474
+ ]);
7475
+ function isBinaryPath(filePath) {
7476
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
7477
+ return BINARY_EXTENSIONS.has(ext);
7478
+ }
7479
+ var fileSummaryCache = /* @__PURE__ */ new Map();
7480
+ var folderListingCache = /* @__PURE__ */ new Map();
7481
+ var fileReadCount = 0;
7482
+ var folderReadCount = 0;
7483
+ function deriveSummary(raw) {
7484
+ let body = raw;
7485
+ let frontmatter = "";
7486
+ if (raw.startsWith("---")) {
7487
+ const end = raw.indexOf("\n---", 3);
7488
+ if (end !== -1) {
7489
+ frontmatter = raw.slice(3, end);
7490
+ const afterClose = raw.indexOf("\n", end + 1);
7491
+ body = afterClose === -1 ? "" : raw.slice(afterClose + 1);
7492
+ }
7493
+ }
7494
+ const fmDescription = frontmatter.split("\n").map((l) => l.trim()).find((l) => /^(description|title):/i.test(l));
7495
+ if (fmDescription) {
7496
+ const value = fmDescription.slice(fmDescription.indexOf(":") + 1).trim().replace(/^["']|["']$/g, "");
7497
+ if (value) return truncateSummary(value);
7498
+ }
7499
+ for (const rawLine of body.split("\n")) {
7500
+ const line = rawLine.trim();
7501
+ if (!line) continue;
7502
+ const cleaned = line.replace(/^#+\s*/, "").replace(/^[-*]\s+/, "").trim();
7503
+ if (cleaned) return truncateSummary(cleaned);
7504
+ }
7505
+ return null;
7506
+ }
7507
+ function truncateSummary(text) {
7508
+ const collapsed = text.replace(/\s+/g, " ").trim();
7509
+ return collapsed.length > SUMMARY_MAX_CHARS ? collapsed.slice(0, SUMMARY_MAX_CHARS - 1).trimEnd() + "\u2026" : collapsed;
7510
+ }
7511
+ async function readFileSummary(filePath) {
7512
+ try {
7513
+ if (isBinaryPath(filePath)) return null;
7514
+ const st = await statWorkspacePath(filePath);
7515
+ if (!st.exists) return null;
7516
+ const mtimeMs = st.mtimeMs;
7517
+ const cached = fileSummaryCache.get(filePath);
7518
+ if (cached && cached.mtimeMs === mtimeMs) {
7519
+ return cached.summary;
7520
+ }
7521
+ const raw = await readWorkspaceFile(filePath);
7522
+ fileReadCount++;
7523
+ const summary = deriveSummary(raw.slice(0, SUMMARY_SCAN_CHARS));
7524
+ fileSummaryCache.set(filePath, { mtimeMs, summary });
7525
+ return summary;
7526
+ } catch {
7527
+ return null;
7528
+ }
7529
+ }
7530
+ async function readFolderListing(folderPath) {
7531
+ try {
7532
+ const st = await statWorkspacePath(folderPath);
7533
+ if (!st.exists) return null;
7534
+ const mtimeMs = st.mtimeMs;
7535
+ const cached = folderListingCache.get(folderPath);
7536
+ if (cached && cached.mtimeMs === mtimeMs) {
7537
+ return cached.listing;
7538
+ }
7539
+ const entries = await readWorkspaceDir(folderPath);
7540
+ folderReadCount++;
7541
+ const listing = `Files: ${entries.join(", ")}`;
6955
7542
  folderListingCache.set(folderPath, { mtimeMs, listing });
6956
7543
  return listing;
6957
7544
  } catch {
@@ -7117,584 +7704,93 @@ function truncatePlanForPrompt(plan) {
7117
7704
  const tail = plan.slice(plan.length - PLAN_TAIL_CHARS);
7118
7705
  return `${head}${PLAN_TRUNCATION_MARKER}${tail}`;
7119
7706
  }
7120
-
7121
- // src/execution/pm-relaunch-instructions.ts
7122
- function resolvePmRelaunchIntent(context, isAuto, agentMode) {
7123
- switch (agentMode) {
7124
- case "building":
7125
- return "build" /* Build */;
7126
- case "review":
7127
- return "review" /* Review */;
7128
- default:
7129
- break;
7130
- }
7131
- if (!isAuto) return "wait_for_team" /* WaitForTeam */;
7132
- return context.plan?.trim() ? "auto_with_plan" /* AutoWithPlan */ : "auto_planning" /* AutoPlanning */;
7133
- }
7134
- function buildRelaunchMessageSummary(context, lastAgentIdx) {
7135
- const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
7136
- if (newMessages.length === 0) {
7137
- return [`You have been relaunched. No new messages since your last session.`];
7138
- }
7139
- return [
7140
- `You have been relaunched. Here are new messages since your last session:`,
7141
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
7142
- ];
7143
- }
7144
- function buildPmBuildRelaunchParts(context, isAuto) {
7145
- const parts = [
7146
- `
7147
- Your plan has been approved. Begin implementing it now.`,
7148
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
7149
- `Start by reading the relevant source files mentioned in the plan, then write code.`,
7150
- `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI.`
7151
- ];
7152
- if (isAuto) {
7153
- parts.push(
7154
- `
7155
- CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or go idle without making code changes.`,
7156
- `Your FIRST action must be reading source files from the plan, then immediately writing code.`,
7157
- `Do NOT summarize the plan or say "ready to implement" \u2014 start implementing.`,
7158
- `If you are genuinely blocked, explain the specific blocker \u2014 do not go idle silently.`
7159
- );
7160
- }
7161
- return parts;
7162
- }
7163
- function buildPmReviewRelaunchParts() {
7164
- return [
7165
- `
7166
- Resume reviewing and coordinating this task.`,
7167
- `Call list_subtasks to check current child-task state and progress.`,
7168
- `Review children in ReviewPR status first, then approve and merge passing PRs.`,
7169
- `Fire next child builds with start_child_cloud_build when ready.`,
7170
- `Do not implement code directly or create a new PR from the PM review session.`
7171
- ];
7172
- }
7173
- function buildPmAutoWithPlanRelaunchParts() {
7174
- return [
7175
- `
7176
- You are in auto mode. A plan already exists for this task.`,
7177
- `Begin implementing it now \u2014 refine story points, title, and tags via update_task_properties if they look like placeholders.`,
7178
- `Do NOT wait for team input \u2014 proceed autonomously.`
7179
- ];
7180
- }
7181
- function buildPmAutoPlanningRelaunchParts() {
7182
- return [
7183
- `
7184
- You are in auto mode. Continue building autonomously.`,
7185
- `No plan is saved on this card yet \u2014 save a concise plan with update_task_plan before writing further code; never pause or wait for approval.`,
7186
- `Do NOT wait for team input \u2014 proceed autonomously.`
7187
- ];
7188
- }
7189
- function buildPmWaitForTeamRelaunchParts() {
7190
- return [
7191
- `
7192
- You are the project manager for this task.`,
7193
- `Review the context above and wait for the team to provide instructions before taking action.`
7194
- ];
7195
- }
7196
- function buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode) {
7197
- const parts = buildRelaunchMessageSummary(context, lastAgentIdx);
7198
- const intent = resolvePmRelaunchIntent(context, isAuto, agentMode);
7199
- const intentPartsByIntent = {
7200
- ["build" /* Build */]: () => buildPmBuildRelaunchParts(context, isAuto),
7201
- ["review" /* Review */]: buildPmReviewRelaunchParts,
7202
- ["auto_with_plan" /* AutoWithPlan */]: buildPmAutoWithPlanRelaunchParts,
7203
- ["auto_planning" /* AutoPlanning */]: buildPmAutoPlanningRelaunchParts,
7204
- ["wait_for_team" /* WaitForTeam */]: buildPmWaitForTeamRelaunchParts
7205
- };
7206
- parts.push(...intentPartsByIntent[intent]());
7207
- return parts;
7208
- }
7209
-
7210
- // src/execution/system-prompt.ts
7211
- import { readFileSync } from "fs";
7212
- import { join as join11 } from "path";
7213
-
7214
- // src/execution/mode-prompt.ts
7215
- var SP_DESC_MAX_CHARS = 80;
7216
- function truncateDescription(desc, maxChars) {
7217
- if (desc.length <= maxChars) return desc;
7218
- return desc.slice(0, maxChars) + "\u2026";
7219
- }
7220
- function formatTagWithContextPaths(tag) {
7221
- const desc = tag.description ? ` \u2014 ${tag.description}` : "";
7222
- const lines = [`- Name: "${tag.name}"${desc}`];
7223
- for (const link of tag.contextPaths ?? []) {
7224
- const label = link.label ? ` (${link.label})` : "";
7225
- lines.push(` \u2192 ${link.type}: ${link.path}${label}`);
7226
- }
7227
- return lines;
7228
- }
7229
- function buildEstimateReassessmentLines(context) {
7230
- const currentSp = context.taskStoryPointValue ?? "unset";
7231
- const currentRisk = context.taskRiskLevel ?? "unset";
7232
- return [
7233
- `Story points and risk are LIVING estimates, not one-time labels. Reassess them at every phase \u2014 planning, building, review \u2014 against your current understanding, and adjust in EITHER direction: work that looks big early is often small once scoped, and vice versa. Lowering an inflated estimate is as valuable as raising an underestimate; a stale value is worse than a changed one.`,
7234
- `Current values: story points ${currentSp}, risk ${currentRisk}.`,
7235
- ``,
7236
- `Risk levels (how much important surface the change touches):`,
7237
- `- critical: foundational surface \u2014 auth, billing, data integrity, migrations`,
7238
- `- high: important surface with broad blast radius`,
7239
- `- medium: moderate, contained surface area`,
7240
- `- low: small or isolated change`
7241
- ];
7242
- }
7243
- function buildPropertyInstructions(context, runnerMode) {
7244
- const isTask = runnerMode === "task";
7245
- const parts = [];
7246
- parts.push(
7247
- ``,
7248
- `### Proactive Property Management`,
7249
- `As you work this task, proactively keep task properties accurate:`,
7250
- `- Use update_task_properties to set any combination of: title, story points, risk, and tags`,
7251
- `- You can update all properties at once or just one at a time as needed`,
7252
- `- Icons are assigned automatically during identification \u2014 do not set icons manually`,
7253
- ``,
7254
- ...buildEstimateReassessmentLines(context),
7255
- ``,
7256
- `Don't wait for the user to ask \u2014 keep these accurate as the work takes shape.`,
7257
- `If scope changes materially at any point, update the properties to match.`
7258
- );
7259
- if (context.storyPoints && context.storyPoints.length > 0) {
7260
- parts.push(``, `Available story point tiers:`);
7261
- for (const sp of context.storyPoints) {
7262
- const desc = sp.description ? ` \u2014 ${truncateDescription(sp.description, SP_DESC_MAX_CHARS)}` : "";
7263
- parts.push(`- Value ${sp.value}: "${sp.name}"${desc}`);
7264
- }
7265
- }
7266
- if (context.projectTags && context.projectTags.length > 0) {
7267
- const assignedIds = new Set(context.taskTagIds ?? []);
7268
- const assigned = context.projectTags.filter((t) => assignedIds.has(t.id));
7269
- const unassigned = context.projectTags.filter((t) => !assignedIds.has(t.id));
7270
- if (assigned.length > 0) {
7271
- parts.push(``, `Assigned tags:`);
7272
- for (const tag of assigned) parts.push(...formatTagWithContextPaths(tag));
7273
- }
7274
- if (!isTask && unassigned.length > 0) {
7275
- parts.push(``, `Available project tags:`);
7276
- for (const tag of unassigned) parts.push(...formatTagWithContextPaths(tag));
7277
- }
7278
- parts.push(
7279
- ``,
7280
- `Tags are the project glossary. call get_tag("<name>") for a term's full spec (overview, linked files, hierarchy) whenever a tag is assigned to this card or mentioned (@[tag:id]) in chat/plans. To mention a tag yourself, write @[tag:<name>] with the tag's exact name \u2014 the server rewrites it to the id token when it stores the text, so you never need the id. An unknown name is left as plain text. When your work changes how a tagged system behaves, update that tag's overview via update_tag with a short reason \u2014 your card is stamped into the revision history.`
7281
- );
7282
- }
7283
- return parts;
7284
- }
7285
- function buildPlanDocumentationSection(context) {
7286
- const hasPlan = !!context?.plan?.trim();
7287
- return [
7288
- ``,
7289
- `### Plan first, then BUILD \u2014 the plan is a step, NOT the deliverable`,
7290
- `- The card is already In Progress and advances automatically (In Progress \u2192 Review PR when you open the PR). There is no plan-approval step.`,
7291
- ...hasPlan ? [
7292
- `- A plan is already saved on the card. Keep it current with update_task_plan if your approach diverges materially \u2014 then IMPLEMENT it in code. The saved plan is not the deliverable; the working implementation is.`
7293
- ] : [
7294
- `- No plan is saved yet: BEFORE writing any code, investigate briefly (search first, read only critical files) and save a concise implementation plan with update_task_plan (file:line citations). Saving the plan is a planning step, not the goal \u2014 immediately move on to WRITING THE CODE that implements it; never pause for approval.`
7295
- ],
7296
- `- Your goal is to BUILD the change, not to produce a plan. After the plan is posted, actually implement it: edit source files, make the change work, then verify. Do NOT stop, go idle, or open a PR the moment the plan exists.`,
7297
- `- Your pull request MUST contain the actual code implementation. Never open a plan-only or empty-diff PR \u2014 a PR that just records the plan is never the goal of auto mode unless the task explicitly asks ONLY for a plan. (If the task genuinely needs no code changes, don't open a PR at all \u2014 finish per the section below.)`,
7298
- `- Identification auto-fills title, story points, and icon with quick AI guesses. After exploring, refine the title, story points, and risk with update_task_properties whenever they no longer match what the work actually is \u2014 adjust in either direction. Icons are automatic \u2014 never set them.`
7299
- ];
7300
- }
7301
- function buildPlanRevisionSection() {
7302
- return [
7303
- ``,
7304
- `### The plan can change while you sleep`,
7305
- `If a turn opens with "The plan changed since this build was dispatched", honor it BEFORE your next Write or Edit:`,
7306
- `1. Call get_current_plan and read the current plan in full.`,
7307
- `2. Compare it against the plan you were launched with \u2014 the current plan wins.`,
7308
- `3. Drop or redo whatever the revision supersedes, and say so with post_to_chat if it invalidates work you already committed.`,
7309
- `Building on a superseded plan is the most expensive mistake a resumed card can make.`
7310
- ];
7311
- }
7312
- function buildNoPrWhenNoCodeSection(baseBranch) {
7313
- const diffCommand = baseDiffCommand(baseBranch);
7314
- return [
7315
- ``,
7316
- `### A PR is NOT required \u2014 only open one for actual code changes`,
7317
- `\`create_pull_request\` is for tasks that change code in the repo. Many tasks don't: support requests, config/credential help, answering a question, investigations, or research whose deliverable is an answer or a file rather than a diff.`,
7318
- `- If you finish the work with NO code changes (an empty \`${diffCommand}\`), do NOT open a PR. An empty or throwaway PR just to "complete" the workflow is wrong \u2014 a human then has to close it.`,
7319
- `- Deliver the result where it belongs: post the answer/config/findings with \`post_to_chat\`, and attach any files the user should keep with \`upload_attachment\` (any file type, up to 25MB). Never publish a deliverable as a Claude artifact or off-platform link \u2014 it belongs on the card.`,
7320
- `- Then complete the card directly with \`force_update_task_status("Complete")\` \u2014 there is no PR or review step for a no-code task.`,
7321
- `- When in doubt, check \`${diffCommand}\`: a real diff means open a PR; no diff means finish in chat and mark Complete.`
7322
- ];
7323
- }
7324
- function buildAttachArtifactsSection() {
7325
- return [
7326
- ``,
7327
- `### Screenshots & recordings \u2014 attach to the card AND embed in the PR description`,
7328
- `If your diff changes anything a user sees rendered (e.g. \`apps/web/**\`, \`apps/sandbox/**\`, \`*.tsx\`/\`*.jsx\`, or styling), capture visual proof BEFORE you open the PR \u2014 a **screenshot** for a static change, or a short **screen recording** (\`.mp4\`/\`.webm\`/\`.gif\`, \u226425MB) for an interaction, animation, or multi-step flow. For a changed screen, capture before AND after. This is part of finishing UI work \u2014 do it unprompted, not only when a human asks. (Non-visual diffs and other documenting files \u2014 diagrams, generated reports \u2014 follow the same pattern when useful.)`,
7329
- `Capture against the running preview: every pod ships headless Playwright chromium baked at \`~/.cache/ms-playwright/\`. Point it at the dev-server preview URL \u2014 \`page.screenshot({ path })\` for an image, a browser context \`recordVideo\` for a \`.webm\`.`,
7330
- `Then attach it with \`upload_attachment\` (pass a workspace path): it reads the codespace-local file and hosts it on the card. It accepts images AND video (\`.mp4\`/\`.webm\`/\`.gif\`), up to 25MB.`,
7331
- `- There is NO "separate pod / no filesystem access" limitation, and it is NOT images-only. Never tell the team a screenshot or recording "could not be hosted" \u2014 attach it.`,
7332
- `- Pass \`tags\` when the capture is a good example of a glossary tag in some state \u2014 a screenshot of the tags page tagged \`tag\`, a board capture tagged \`task\`. Each tag's page lists its recent tagged attachments, so the label turns your screenshot into a living example of that entity and makes visual drift easy to spot. An unmatched name is reported back and never fails the upload.`,
7333
- ``,
7334
- `**The PR description must show the visual proof, not just point at it.** \`upload_attachment\` returns a \`downloadUrl\` for every file it stores, plus a ready-to-paste markdown line. Copy that line into the \`body\` you pass to \`create_pull_request\`:`,
7335
- `- Images embed inline: \`![before \u2014 task board](<downloadUrl>)\`. Write a real caption in the alt text \u2014 "before"/"after", or what the shot demonstrates.`,
7336
- `- Video and other non-image files do not render inline on GitHub. Link them instead: \`[interaction recording](<downloadUrl>)\`.`,
7337
- `- Put them under a \`## Screenshots\` heading in the PR body, with before/after side by side (or one after the other) when you changed an existing screen.`,
7338
- `- The URL is a signed capability our API serves; GitHub fetches and caches the image through its own proxy when the description renders, so it keeps working after the capability expires. Do NOT paste codespace-local file paths (\`/workspaces/repo/shot.png\`) \u2014 reviewers cannot open those.`,
7339
- `- Keep attaching to the card as well. The card is the durable home for the capture and the only place \`tags\` apply; the PR description is what the reviewer actually reads. Do both \u2014 neither one replaces the other.`,
7340
- `- If \`upload_attachment\` returns no \`downloadUrl\`, still attach to the card, and say plainly in the PR body that the embed URL was unavailable. Never silently ship a UI PR with no visual proof.`
7341
- ];
7342
- }
7343
- function buildPrGuideSection() {
7344
- return [
7345
- ``,
7346
- `### PR Guide \u2014 publish when you open the PR, refresh it on every push`,
7347
- `Once your PR is open, publish a structured guide that walks a reviewer through the change with \`publish_review_guide\`. This is part of opening a PR: call it right after \`create_pull_request\` succeeds, and again whenever you push more commits to the branch.`,
7348
- `- The head SHA is resolved from the local repo for you, so never hand-expand an abbreviated hash from commit output or plan text. Publish AFTER the push that created/updated the PR has landed on the branch.`,
7349
- `- \`sections\` is a real top-level array ARGUMENT, not prose inside \`overview\`. Keep \`overview\` under 3000 characters \u2014 a short intro only \u2014 and put the walkthrough in \`sections\`, ordered with core behavior first and tests, migrations, generated files, and configuration later unless they are central.`,
7350
- `- Explain each section's purpose and effect in plain language, and reference ONLY files this PR's diff actually changed \u2014 never context files you merely read or discussed.`,
7351
- `- Anchors are optional and strict: the safest \`files\` entry is \`{"path": "..."}\` alone. If you anchor, \`hunkHeader\` must be the byte-exact full line from \`git diff <base>..HEAD -- <file> | grep '^@@'\` INCLUDING the trailing context text after the second \`@@\`, and \`startLine\`/\`endLine\` must overlap one real hunk's new-file range. When in doubt, omit the anchor fields.`,
7352
- `- After any later push, call \`publish_review_guide\` again for the NEW head SHA \u2014 the card shows a "stale" banner until you republish for the current head.`,
7353
- `- Best-effort: if publication fails after one corrected retry, continue. It never blocks opening or updating the PR.`
7354
- ];
7707
+
7708
+ // src/execution/pm-relaunch-instructions.ts
7709
+ function branchLine(context) {
7710
+ return `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`;
7355
7711
  }
7356
- function buildPrePrProtocolSection(context) {
7357
- const base = context?.baseBranch?.trim() || "dev";
7358
- return [
7359
- `### Pre-PR Protocol \u2014 sync the base first, then ONE verification pass`,
7360
- `CI runs the FULL suite (lint, typecheck, all test shards) on every PR \u2014 do not duplicate it locally. Local gates run ONCE, in this order:`,
7361
- `1. Finish the implementation and commit it.`,
7362
- `2. **Sync the base FIRST**: \`git fetch origin ${base} && git merge origin/${base} --no-edit\`. Run \`bun install\` afterwards only if the merge touched \`package.json\`/\`bun.lock\`. Merging before the gates is what makes one pass sufficient \u2014 merging after them is what forces a second run.`,
7363
- `3. **One verification pass**: \`bun run check\` (lint + typecheck, fast, not lock-wrapped) then \`bun run test:affected\` (runs only the tests your diff can affect \u2014 apps/api diffs are expected to run unit tests only since CI covers the int shards; \`packages/shared\`/\`packages/db\`/root-config diffs escalate to the full suite automatically, which is still one pass, not a repeat). Selection is a heuristic \u2014 confirm it actually ran the packages you changed.`,
7364
- `4. **Open the PR immediately** with \`mcp__conveyor__create_pull_request\`. Do NOT re-merge the base and do NOT re-run a gate that already passed. If \`${base}\` moved while the gates were running, open the PR anyway \u2014 CI validates against the merged base.`,
7365
- `Docs/markdown/\`.claude\`-only diffs skip steps 2-3 entirely \u2014 open the PR and let CI validate.`,
7366
- ``,
7367
- `**When a gate fails: fix, re-run only the failing tests, confirm once.**`,
7368
- `- Iterate on the failures directly with the package-local script \u2014 \`bun run --cwd <pkg> test:unit <files>\` (or \`test <files>\`). It skips turbo's dep-build and the singleton lock, so it costs seconds instead of minutes. Do NOT re-run the whole gate on each iteration.`,
7369
- `- Pick that file set deliberately: \`rg <ComponentName>\` across \`__tests__\` to find every suite that transitively mounts what you changed, so the targeted loop is not blind to indirect mounters.`,
7370
- `- Once the targeted files pass, run the full scoped gate ONE more time as final confirmation, then open the PR. That single confirmation run \u2014 not a full gate per iteration \u2014 is what catches a suite your targeted set missed.`,
7371
- `- Do NOT open a PR with a known failing gate.`,
7372
- ...gateFailureModes(),
7373
- ``,
7374
- `Also before you open the PR (neither one needs a gate re-run):`,
7375
- `- Reassess story points and risk against the ACTUAL diff \u2014 the estimate was made at planning time; now you know what the work really was. Correct them with \`update_task_properties\` in either direction.`,
7376
- `- **UI/UX diffs:** if you changed anything rendered (\`apps/web\`, \`apps/sandbox\`, \`*.tsx\`/\`*.jsx\`, styling), capture a screenshot (static change) or short screen recording (interaction/animation), attach it to the card with \`upload_attachment\`, and **embed the returned \`downloadUrl\` in the PR description** \u2014 both, not either \u2014 see "Screenshots & recordings" below. Skip only when the diff has no visual effect.`,
7377
- `- For refactors: run \`${baseDiffCommand(context?.baseBranch)}\` and confirm the public API surface (exports, function signatures) has no unintended breaking changes.`,
7378
- ``,
7379
- `### Triaging a CI failure`,
7380
- ...ciTriageChecklist(context?.baseBranch)
7381
- ];
7712
+ function resolvePmRelaunchIntent(context, isAuto, agentMode) {
7713
+ switch (agentMode) {
7714
+ case "building":
7715
+ return "build" /* Build */;
7716
+ case "review":
7717
+ return "review" /* Review */;
7718
+ default:
7719
+ break;
7720
+ }
7721
+ if (!isAuto) return "wait_for_team" /* WaitForTeam */;
7722
+ return context.plan?.trim() ? "auto_with_plan" /* AutoWithPlan */ : "auto_planning" /* AutoPlanning */;
7382
7723
  }
7383
- function buildExplorationMethodology() {
7724
+ function buildRelaunchMessageSummary(context, lastAgentIdx) {
7725
+ const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
7726
+ if (newMessages.length === 0) {
7727
+ return [`You have been relaunched. No new messages since your last session.`];
7728
+ }
7384
7729
  return [
7385
- ``,
7386
- `### Exploration Methodology`,
7387
- `Investigate efficiently \u2014 do not read files aimlessly:`,
7388
- `- Search first, read second: use grep/glob to locate relevant code, then read only the files that matter`,
7389
- `- Never re-read a file already in your context \u2014 you have a large context window, scroll up instead`,
7390
- `- Start with 3-5 critical files, form a hypothesis about the approach, then validate with targeted reads`,
7391
- `- Stop exploring when you can cite specific \`file.ts:line\` locations and function names for every step in your plan \u2014 that's enough`
7730
+ `You have been relaunched. Here are new messages since your last session:`,
7731
+ ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
7392
7732
  ];
7393
7733
  }
7394
- function buildPlanCitationFormat() {
7734
+ function buildPmBuildRelaunchParts(context) {
7395
7735
  return [
7396
- ``,
7397
- `### Plan Citation Format`,
7398
- `Plans must ground each change in the code. For every step that touches code:`,
7399
- `- Cite the exact location as \`path/from/repo/root.ts:lineNumber\` (e.g. \`packages/conveyor-agent/src/execution/mode-prompt.ts:129\`).`,
7400
- `- Name the specific function, class, constant, or JSX element being touched.`,
7401
- `- When behavior hinges on a short piece of code, quote 1\u20133 lines inline instead of paraphrasing.`,
7402
- `- Ranges are fine for larger edits (\`foo.ts:120-145\`). Do not cite whole files without a line.`,
7403
- `- If a file doesn't exist yet, write \`NEW: path/to/new-file.ts\` and describe the surrounding module it fits into.`
7736
+ `
7737
+ Your plan has been approved \u2014 you are the Builder on this card now.`,
7738
+ ...buildSkillInvocation("/conveyor-build"),
7739
+ branchLine(context),
7740
+ `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI.`
7404
7741
  ];
7405
7742
  }
7406
- function buildDiscoveryPrompt(context, runnerMode) {
7407
- const parts = [
7743
+ function buildPmReviewRelaunchParts() {
7744
+ return [
7408
7745
  `
7409
- ## Mode: Discovery`,
7410
- `You are in Discovery mode \u2014 helping plan and scope this task.`,
7411
- `- You have read-only codebase access (can read files, run git commands, search code)`,
7412
- `- Shell commands run without asking for approval \u2014 the pod is a sandbox, so investigate freely (build, test, query the dev DB, read logs). Only commands that would start the work are denied: git commit/push/merge/rebase/checkout, git apply, gh pr create, and package publish`,
7413
- `- You can write plan files in .claude/plans/ only \u2014 no other file writes. Plan files on disk are NOT synced to the task; save the plan via update_task_plan`,
7414
- `- Do NOT attempt to edit, write, or modify source code files \u2014 these operations will be denied`,
7415
- `- If you identify code changes needed, describe them in the plan instead of implementing them`,
7416
- `- You can create and manage subtasks`,
7417
- `- Goal: collaborate with the user to create a clear plan`,
7418
- `- Proactively fill task properties (SP, tags) as the plan takes shape`,
7419
- ``,
7420
- `### Planning Checklist (complete ALL before calling ExitPlanMode)`,
7421
- `Your PRIMARY goal is to create a thorough plan. Complete these steps in order:`,
7422
- `1. Read the task description and chat history \u2014 respond to what's been discussed`,
7423
- `2. Investigate the codebase using the methodology below \u2014 search first, read targeted files`,
7424
- `3. Save a detailed plan via \`update_task_plan\``,
7425
- `4. Set story points, risk, tags, and title via \`update_task_properties\` (icon is set automatically)`,
7426
- `5. Discuss the plan with the team if they're engaged, incorporate feedback`,
7427
- `6. THEN call ExitPlanMode \u2014 it is the LAST step, not the first`,
7428
- ...buildExplorationMethodology(),
7429
- ...buildPlanCitationFormat(),
7430
- ``,
7431
- `### Self-Identification Tools`,
7432
- `Use these MCP tools to set your own task properties:`,
7433
- `- \`update_task_plan\` \u2014 save your plan and description`,
7434
- `- \`update_task_properties\` \u2014 set title, story points, risk, and tags (any combination)`,
7435
- `Note: Icons are assigned automatically during identification after planning is complete.`,
7436
- ``,
7437
- `### Tags & Context`,
7438
- `- Early in discovery, identify relevant project tags that match this task's domain`,
7439
- `- Add matching tags using \`update_task_properties\` \u2014 this links relevant documentation and rules that help you plan more effectively`,
7440
- `- Tags accelerate discovery by surfacing domain-specific context automatically`,
7441
- ``,
7442
- ...context?.isParentTask ? [
7443
- `### Parent Task Coordination`,
7444
- `You are a parent task with child tasks. Focus on breaking work into child tasks with detailed plans, not planning implementation for yourself.`,
7445
- `- Use \`list_subtasks\` to review existing children. Create or update child tasks using \`create_subtask\` / \`update_subtask\`.`,
7446
- `- Each child task should be a self-contained unit of work with a clear plan.`,
7447
- `- Set ordering as explicit **card metadata**, not prose: pass \`dependsOn\` (sibling ids/slugs) on \`create_subtask\` for any child that blocks on another. Leave independent children with no dependencies so the pack runner fans them out in parallel. Do NOT encode "do X after Y" only in the plan text \u2014 the runner schedules off dependsOn, not narrative.`
7448
- ] : [
7449
- `### Self-Update vs Subtasks`,
7450
- `- If the work fits in a single task (1-3 SP), update YOUR OWN plan and properties \u2014 do not create subtasks`,
7451
- `- Only create subtasks when the work genuinely requires multiple independent pieces (e.g., Pack-tier work, 8+ SP)`
7452
- ],
7453
- ``,
7454
- `### Subtask Plan Requirements`,
7455
- `When creating subtasks, each MUST include a detailed \`plan\` field:`,
7456
- `- Plans should be multi-step implementation guides, not vague descriptions`,
7457
- `- Include concrete \`file.ts:line\` citations, function/symbol names, and short code snippets where relevant`,
7458
- `- Reference existing implementations when relevant (e.g., "follow the pattern in src/services/foo.ts")`,
7459
- `- Include testing requirements and acceptance criteria`,
7460
- `- Set \`storyPointValue\` based on estimated complexity`,
7461
- `- Express cross-child ordering as \`dependsOn\` (sibling ids/slugs) on \`create_subtask\` \u2014 explicit dependency metadata, not "after task 2" phrasing in the plan. Independent children get no dependencies so they run in parallel.`,
7462
- ``,
7463
- `### Plan Verification Requirements`,
7464
- `Every plan MUST include a **Testing / Verification** section so the build agent has a clear definition of "done". Enumerate:`,
7465
- `- The scoped verification for the change: \`bun run check\` (lint + typecheck) plus \`bun run test:affected\` \u2014 name the specific package suites the diff will touch. Docs-only plans should state that no local gates are needed (CI validates on the PR).`,
7466
- `- Any task-specific end-to-end checks (manual UI walk-through, API smoke test, migration dry-run, etc.)`,
7467
- `You are NOT expected to run these gates yourself \u2014 discovery is read-only. Just describe them in the plan.`,
7468
- ``,
7469
- `### Completing Planning`,
7470
- `Once ALL checklist items above are done, call the **ExitPlanMode** tool.`,
7471
- `- Required before ExitPlanMode will succeed: **plan** (via update_task_plan), **story points**, **risk**, and **title** (via update_task_properties)`,
7472
- `- ExitPlanMode validates these properties and marks planning as complete`,
7473
- `- It does NOT start building \u2014 the team controls when to switch to Build mode`,
7474
- `- Do NOT call ExitPlanMode until you have thoroughly explored the codebase and saved a detailed plan`
7746
+ Resume reviewing and coordinating this task.`,
7747
+ `Call list_subtasks to check current child-task state and progress.`,
7748
+ `Review children in ReviewPR status first, then approve and merge passing PRs.`,
7749
+ `Fire next child builds with start_child_cloud_build when ready.`,
7750
+ `Do not implement code directly or create a new PR from the PM review session.`
7475
7751
  ];
7476
- if (context) parts.push(...buildPropertyInstructions(context, runnerMode));
7477
- return parts.join("\n");
7478
7752
  }
7479
- function buildAutoPrompt(context, runnerMode) {
7480
- const parts = [
7753
+ function buildPmAutoWithPlanRelaunchParts(context) {
7754
+ return [
7481
7755
  `
7482
- ## Mode: Auto`,
7483
- `You are in Auto mode \u2014 operating autonomously through building \u2192 PR.`,
7484
- `- You have full coding access (read, write, edit, bash, git) from the start \u2014 there is no read-only planning phase`,
7485
- `- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
7486
- ...buildPlanDocumentationSection(context),
7487
- ``,
7488
- `### Subtask Plan Requirements`,
7489
- `When creating subtasks, each MUST include a detailed \`plan\` field:`,
7490
- `- Plans should be multi-step implementation guides, not vague descriptions`,
7491
- `- Include concrete \`file.ts:line\` citations, function/symbol names, and short code snippets where relevant`,
7492
- `- Reference existing implementations when relevant`,
7493
- `- Include testing requirements and acceptance criteria`,
7494
- `- Set \`storyPointValue\` based on estimated complexity`,
7495
- `- Express cross-child ordering as \`dependsOn\` (sibling ids/slugs) on \`create_subtask\` \u2014 explicit dependency metadata, not "after task 2" phrasing in the plan. Independent children get no dependencies so they run in parallel.`,
7496
- ``,
7497
- ...buildPlanCitationFormat(),
7498
- ``,
7499
- ...context?.isParentTask ? [
7500
- ``,
7501
- `### Parent Task Guidance`,
7502
- `You are a parent task \u2014 coordinate child tasks instead of implementing directly.`,
7503
- `If no children exist yet, break the work down now: save a parent-level plan with update_task_plan, then create child tasks with create_subtask (each with a detailed plan).`,
7504
- `Child task status lifecycle: Open \u2192 InProgress \u2192 ReviewPR \u2192 ReviewDev \u2192 Complete.`,
7505
- `Set child ordering with \`dependsOn\` (sibling ids/slugs) on \`create_subtask\` \u2014 explicit metadata the pack runner schedules off, not order described in plan text. Independent children get none and run in parallel.`
7506
- ] : [
7507
- ...buildAttachArtifactsSection(),
7508
- ...buildPrGuideSection(),
7509
- ...buildNoPrWhenNoCodeSection(context?.baseBranch)
7510
- ],
7511
- ...buildPlanRevisionSection(),
7512
- ``,
7513
- `### Autonomous Guidelines:`,
7514
- `- Make decisions independently \u2014 do not ask the team for approval at each step`,
7515
- `- Only escalate when genuinely blocked (ambiguous requirements, missing access, conflicting instructions)`,
7516
- `- Investigate efficiently: search (grep/glob) to locate code, read only critical files, form a hypothesis, validate, then plan`
7756
+ A plan already exists for this card \u2014 you are the Builder on it.`,
7757
+ ...buildSkillInvocation("/conveyor-build"),
7758
+ branchLine(context)
7517
7759
  ];
7518
- if (context) parts.push(...buildPropertyInstructions(context, runnerMode));
7519
- return parts.join("\n");
7520
7760
  }
7521
- function buildBuildingPrompt(context) {
7522
- const parts = [
7761
+ function buildPmAutoPlanningRelaunchParts(context) {
7762
+ return [
7523
7763
  `
7524
- ## Mode: Building`,
7525
- `You are in Building mode \u2014 executing the plan.`,
7526
- `- You have full coding access (read, write, edit, bash, git)`,
7527
- `- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
7528
- ...context?.isParentTask ? [
7529
- `- You are a parent task. Use \`list_subtasks\`, \`start_child_cloud_build\`, and subtask management tools to coordinate children.`,
7530
- `- Do NOT implement code directly \u2014 fire child builds and review their work.`,
7531
- `- Goal: coordinate child task execution and ensure all children complete successfully`
7532
- ] : [
7533
- `- If this is a leaf task (no children): execute the plan directly`,
7534
- `- Goal: implement the plan, run scoped verification once, open a PR when done`,
7535
- ``,
7536
- ...buildPrePrProtocolSection(context),
7537
- ...buildAttachArtifactsSection(),
7538
- ...buildPrGuideSection(),
7539
- ...buildNoPrWhenNoCodeSection(context?.baseBranch),
7540
- ...context?.isAuto || !context?.plan?.trim() ? buildPlanDocumentationSection(context) : []
7541
- ],
7542
- ...buildPlanRevisionSection()
7764
+ No plan is saved on this card yet.`,
7765
+ ...buildSkillInvocation("/conveyor-plan"),
7766
+ `Save it with update_task, then continue with \`/conveyor-build\`.`,
7767
+ branchLine(context)
7543
7768
  ];
7544
- if (context) parts.push(...buildPropertyInstructions(context));
7545
- return parts.join("\n");
7546
- }
7547
- function buildModePrompt(agentMode, context, runnerMode) {
7548
- switch (agentMode) {
7549
- case "discovery":
7550
- return buildDiscoveryPrompt(context, runnerMode);
7551
- case "building":
7552
- return buildBuildingPrompt(context);
7553
- case "review":
7554
- return buildReviewPrompt(context);
7555
- case "auto":
7556
- return buildAutoPrompt(context, runnerMode);
7557
- case "chat":
7558
- return buildChatPrompt(context);
7559
- default:
7560
- return null;
7561
- }
7562
7769
  }
7563
- function buildChatPrompt(context) {
7564
- const base = context?.baseBranch?.trim() || "dev";
7770
+ function buildPmWaitForTeamRelaunchParts() {
7565
7771
  return [
7566
7772
  `
7567
- ## Mode: Chat`,
7568
- `You are in Chat mode \u2014 a conversational assistant working directly with the user on this card.`,
7569
- `- Respond conversationally to the user in chat. Ask clarifying questions when useful; this is a back-and-forth, not an autonomous build.`,
7570
- `- You have full read/write access to the workspace and can run non-destructive shell commands, so you CAN create files (notes, scripts, docs, data, diagrams, etc.) when they help the user.`,
7571
- `- This card starts Unidentified. That is correct \u2014 it stays a conversation until the work becomes a development task (see below).`,
7572
- ``,
7573
- `### Deliverables \u2014 attach files to the card`,
7574
- `- When you create a file the user should keep, attach it to the card with the \`upload_attachment\` tool so it shows up on the card. Mention in chat what you attached.`,
7575
- `- \`upload_attachment\` accepts any file type (markdown, PDF, HTML, text, data, images) up to 25MB \u2014 this is how docs are delivered. Do NOT publish deliverables as a Claude artifact or any other off-platform link; the card is the system of record.`,
7576
- `- For conversational work, attachments are the deliverable. Do NOT open a PR to hand over a document or an answer.`,
7577
- ``,
7578
- `### Turning into a development task`,
7579
- `- If the conversation becomes a request to change the code, escalate the card first: save a plan with \`update_task_plan\`. That first plan identifies the card and moves it to In Progress.`,
7580
- `- The plan is also the gate on your PR path: \`create_pull_request\`, \`git push\` and \`gh pr\` are DENIED until a plan is saved, and allowed afterwards. Do not try to work around a denial \u2014 save the plan.`,
7581
- `- Once the plan is saved, work like a normal build: implement it, then run ONE verification pass (\`bun run check\` plus \`bun run test:affected\`) after syncing the base (\`git fetch origin ${base} && git merge origin/${base} --no-edit\`), then open the PR with \`mcp__conveyor__create_pull_request\`.`,
7582
- `- Keep talking to the user while you build \u2014 this is still their card.`,
7583
- ``,
7584
- `### Finishing the conversation`,
7585
- `- When the user indicates they are done (or explicitly asks to wrap up / close the card), call \`force_update_task_status\` with status \`"Complete"\`. It works from either status, so a card still sitting Unidentified can be completed the same way as one that reached In Progress.`,
7586
- `- If you opened a PR, do NOT complete the card \u2014 the PR review flow takes it from there.`,
7587
- `- If the user is still engaged, keep helping \u2014 only complete the card once the interaction has concluded.`,
7588
- `- Do not complete the card while you still owe the user a response or an attachment.`
7589
- ].join("\n");
7773
+ You are the project manager for this task.`,
7774
+ `Review the context above and wait for the team to provide instructions before taking action.`
7775
+ ];
7590
7776
  }
7591
- function buildReviewTagSection(context) {
7592
- const assignedIds = new Set(context?.taskTagIds ?? []);
7593
- const assigned = (context?.projectTags ?? []).filter((t) => assignedIds.has(t.id));
7594
- if (assigned.length === 0) return [];
7595
- const parts = [`### Card Tags & Domain Context`, `This card carries these project glossary tags:`];
7596
- for (const tag of assigned) parts.push(...formatTagWithContextPaths(tag));
7597
- parts.push(
7598
- ``,
7599
- `A tag's rules and overview define the conventions the code under review must follow. Use them when you judge Pattern Consistency:`,
7600
- `- Read the tag entries that match the files in this diff. The "Reference Guides" section of the task brief lists the same tags with a one-line summary of each linked doc.`,
7601
- `- Call get_tag("<name>") for a term's full spec (overview, linked files, hierarchy) when the linked docs are not enough.`,
7602
- `- A change that contradicts a tagged system's documented conventions is a review finding, even when it reads fine on its own.`,
7603
- `- Skip the tags this diff does not touch \u2014 do not read them all up front.`,
7604
- `- If the diff changes how a tagged system behaves and the tag's overview is now wrong, say so in your review.`,
7605
- ``
7606
- );
7777
+ function buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode) {
7778
+ const parts = buildRelaunchMessageSummary(context, lastAgentIdx);
7779
+ const intent = resolvePmRelaunchIntent(context, isAuto, agentMode);
7780
+ const intentPartsByIntent = {
7781
+ ["build" /* Build */]: () => buildPmBuildRelaunchParts(context),
7782
+ ["review" /* Review */]: buildPmReviewRelaunchParts,
7783
+ ["auto_with_plan" /* AutoWithPlan */]: () => buildPmAutoWithPlanRelaunchParts(context),
7784
+ ["auto_planning" /* AutoPlanning */]: () => buildPmAutoPlanningRelaunchParts(context),
7785
+ ["wait_for_team" /* WaitForTeam */]: buildPmWaitForTeamRelaunchParts
7786
+ };
7787
+ parts.push(...intentPartsByIntent[intent]());
7607
7788
  return parts;
7608
7789
  }
7609
- function buildReviewPrompt(context) {
7610
- const parts = [
7611
- `
7612
- ## Mode: Review`,
7613
- `You are in Review mode \u2014 performing code review with fix capability.`,
7614
- `- You have full write access \u2014 you can audit code, make fixes, push changes, and run tests`,
7615
- `- Safety rules: no destructive operations, use --force-with-lease instead of --force`,
7616
- ``
7617
- ];
7618
- if (context?.isParentTask) {
7619
- parts.push(
7620
- `### Parent Task Review`,
7621
- `You are reviewing and coordinating child tasks.`,
7622
- `- Use \`list_subtasks\` to see current child task state and progress.`,
7623
- `- For children in ReviewPR status: review their code quality and merge with \`approve_and_merge_pr\`.`,
7624
- `- For children with failing CI: check with \`get_execution_logs(childTaskId)\` and escalate if stuck.`,
7625
- `- Fire next child builds with \`start_child_cloud_build\` when ready.`,
7626
- `- Create follow-up tasks for issues discovered during review.`,
7627
- ``,
7628
- `### Coordination Workflow`,
7629
- `1. Check child task statuses with \`list_subtasks\``,
7630
- `2. Review completed children \u2014 check PRs, run tests if needed`,
7631
- `3. Approve and merge passing PRs`,
7632
- `4. Fire builds for children that are ready`,
7633
- `5. Create follow-up tasks for anything out of scope`,
7634
- `6. As children complete, correct their story points with update_subtask (storyPointValue) when the actual work diverged from the estimate \u2014 either direction`
7635
- );
7636
- } else {
7637
- const tagSection = buildReviewTagSection(context);
7638
- const patternConsistency = tagSection.length ? `- **Pattern Consistency**: Does the code follow existing patterns in the codebase? Check nearby files AND the card's tag rules/overviews (see "Card Tags & Domain Context" below).` : `- **Pattern Consistency**: Does the code follow existing patterns in the codebase? Check nearby files.`;
7639
- parts.push(
7640
- `### Code Review Process`,
7641
- `1. Run \`${baseDiffCommand(context?.baseBranch)}\` to see all changes in this PR`,
7642
- `2. Read the task plan to understand the intended changes`,
7643
- `3. Explore the surrounding codebase to verify pattern consistency`,
7644
- `4. Review against the criteria below`,
7645
- ``,
7646
- `### Review Criteria`,
7647
- `- **Correctness**: Does the code do what the plan says? Logic errors, off-by-one, race conditions?`,
7648
- patternConsistency,
7649
- `- **Security**: No hardcoded secrets, no injection vulnerabilities, proper input validation at boundaries.`,
7650
- `- **Performance**: No unnecessary loops, no N+1 queries, no blocking in async contexts.`,
7651
- `- **Error Handling**: Appropriate error handling at system boundaries. No swallowed errors.`,
7652
- `- **Test Coverage**: Are new code paths tested? Edge cases covered?`,
7653
- `- **TypeScript Best Practices**: Proper typing (no unnecessary \`any\`), correct React patterns, proper async/await.`,
7654
- `- **Naming & Readability**: Clear names, no misleading comments, self-documenting code.`,
7655
- ``,
7656
- `### Fix Capability`,
7657
- `You have full write access. If you find issues:`,
7658
- `- **Small fixes**: Make the fix directly, commit, and push. Then re-review.`,
7659
- `- **Larger issues**: Use \`request_code_changes\` to flag them for the team.`,
7660
- `- After pushing fixes, wait for CI to pass before approving.`,
7661
- ``,
7662
- `### Output \u2014 You MUST do exactly ONE of:`,
7663
- ``,
7664
- `#### If code passes review (or after you've fixed all issues):`,
7665
- `Use the \`approve_code_review\` tool with a brief summary of what looks good.`,
7666
- ``,
7667
- `#### If changes are needed that you cannot fix:`,
7668
- `Use the \`request_code_changes\` tool with specific issues:`,
7669
- `- Reference specific files and line numbers`,
7670
- `- Explain what's wrong and suggest fixes`,
7671
- `- Focus on substantive issues, not style nitpicks (linting handles that)`,
7672
- ``,
7673
- `#### Risk level (required on BOTH tools):`,
7674
- `Every verdict MUST include a \`risk\` level \u2014 judge it by the surface area the change touches:`,
7675
- `- \`critical\`: touches critical/foundational surface (auth, billing, data integrity, migrations)`,
7676
- `- \`high\`: touches important surface with broad blast radius`,
7677
- `- \`medium\`: moderate, contained surface area`,
7678
- `- \`low\`: small or isolated change`,
7679
- `The task may already have a risk level set. If your review makes you disagree with it, set the level you believe is correct \u2014 you have the authority to override it in either direction.`,
7680
- ``,
7681
- `#### Story points (correct if wrong):`,
7682
- `You can see the full final diff \u2014 if the story-point estimate no longer matches the actual size of the work, correct it with update_task_properties (storyPointValue), in either direction. The card should record what the work actually was, not what it looked like at planning time.`,
7683
- ``,
7684
- ...tagSection,
7685
- `### Previous Review Feedback`,
7686
- `If previous review feedback is present in the chat history, verify those specific issues were addressed before raising new concerns.`,
7687
- ``,
7688
- `### Rules`,
7689
- `- Do NOT re-review things CI already validates (formatting, lint rules).`,
7690
- `- Be concise \u2014 actionable specifics over general observations.`,
7691
- `- Max 5-7 issues per review. Prioritize the most important ones.`
7692
- );
7693
- }
7694
- return parts.join("\n");
7695
- }
7696
7790
 
7697
7791
  // src/execution/system-prompt.ts
7792
+ import { readFileSync } from "fs";
7793
+ import { join as join11 } from "path";
7698
7794
  function repoHasScript(workspaceDir, script) {
7699
7795
  try {
7700
7796
  const pkg = JSON.parse(readFileSync(join11(workspaceDir, "package.json"), "utf8"));
@@ -7722,7 +7818,7 @@ Environment (ready, no setup required):`,
7722
7818
  `
7723
7819
  Workflow:`,
7724
7820
  `- You can draft and iterate on plans in .claude/plans/*.md, but files on disk are NOT synced to the task.`,
7725
- `- Save the plan to the task with update_task_plan \u2014 this is the only way the plan is persisted.`,
7821
+ `- Save the plan to the task with update_task \u2014 this is the only way the plan is persisted.`,
7726
7822
  `- After saving the plan, call post_to_chat with a short summary for the team (your turn output is NOT posted to chat \u2014 they only see what you post), then end your turn. Do NOT attempt to execute the plan yourself.`,
7727
7823
  `- A separate task agent will handle execution after the team reviews and approves your plan.`
7728
7824
  ];
@@ -7749,7 +7845,7 @@ function buildActivePreamble(context, workspaceDir) {
7749
7845
  `You are an AI project manager in ACTIVE mode for the "${context.title}" project.`,
7750
7846
  `You have direct coding access to the repository at ${workspaceDir}.`,
7751
7847
  `You can edit files, run tests, and make commits.`,
7752
- `You still have access to all PM tools (subtasks, update_task_plan, chat).`,
7848
+ `You still have access to all PM tools (subtasks, update_task, chat).`,
7753
7849
  `
7754
7850
  Environment (ready, no setup required):`,
7755
7851
  `- Repository is cloned at your current working directory.`,
@@ -7804,12 +7900,15 @@ Git:`,
7804
7900
  `- If \`git push\` is rejected as non-fast-forward, run \`git push --force-with-lease origin ${context.githubBranch}\`. This branch is exclusively yours, so force-with-lease is safe.`
7805
7901
  ];
7806
7902
  }
7903
+ function buildPackPrompt(mode, context, config, setupLog) {
7904
+ return mode === "pack" && config.packExecution !== "fan-out" ? buildSinglePodPackPrompt(context, config, setupLog) : buildPackRunnerSystemPrompt(context, config, setupLog);
7905
+ }
7807
7906
  function buildSystemPrompt(mode, context, config, setupLog, agentMode) {
7808
7907
  const isPm = mode === "pm";
7809
7908
  const isPmActive = isPm && agentMode === "building";
7810
7909
  const isPackRunner = mode === "pack" || isPm && !!config.isAuto && !!context.isParentTask;
7811
7910
  if (isPackRunner) {
7812
- return buildPackRunnerSystemPrompt(context, config, setupLog);
7911
+ return buildPackPrompt(mode, context, config, setupLog);
7813
7912
  }
7814
7913
  const parts = isPmActive ? buildActivePreamble(context, config.workspaceDir) : isPm ? buildPmPreamble(context) : buildTaskAgentPreamble(context, config.workspaceDir);
7815
7914
  if (setupLog.length > 0) {
@@ -7869,6 +7968,39 @@ function detectRelaunchScenario(context, trustChatHistory = false) {
7869
7968
  const hasNewUserMessages = messagesAfterAgent.some((m) => m.role === "user");
7870
7969
  return hasNewUserMessages ? "feedback_relaunch" : "idle_relaunch";
7871
7970
  }
7971
+ function branchRule(context, forReview = false) {
7972
+ return forReview ? `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire review. Do not checkout or create other branches.` : `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`;
7973
+ }
7974
+ function newMessagesBlock(messages) {
7975
+ if (messages.length === 0) return [];
7976
+ return [
7977
+ `
7978
+ New messages since your last run:`,
7979
+ ...messages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
7980
+ ];
7981
+ }
7982
+ function pullRequestLine(context) {
7983
+ return context.githubPRUrl ? `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch to update it. Do NOT create a new PR.` : `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`;
7984
+ }
7985
+ function isPlanningSession(mode, agentMode, isAuto) {
7986
+ if (mode === "plan") return true;
7987
+ if (agentMode === "discovery" || agentMode === "help") return true;
7988
+ if (mode !== "pm") return false;
7989
+ return !(agentMode === "building" || agentMode === "auto" || !!isAuto);
7990
+ }
7991
+ function builderKickoff(context, role) {
7992
+ const parts = [role, ...buildSkillInvocation("/conveyor-build"), branchRule(context)];
7993
+ if (!context.plan?.trim()) {
7994
+ parts.push(
7995
+ `No plan is saved on this card yet \u2014 run \`/conveyor-plan\` first and save it with update_task, then continue with \`/conveyor-build\`.`
7996
+ );
7997
+ }
7998
+ parts.push(pullRequestLine(context));
7999
+ return parts;
8000
+ }
8001
+ function plannerKickoff(role) {
8002
+ return [role, ...buildSkillInvocation("/conveyor-plan")];
8003
+ }
7872
8004
  function buildRelaunchWithSession(mode, context, agentMode, isAuto) {
7873
8005
  const scenario = detectRelaunchScenario(context);
7874
8006
  const hasPriorTurn = !!context.lastSeenMessageId || !!context.claudeSessionId;
@@ -7887,46 +8019,32 @@ function buildRelaunchWithSession(mode, context, agentMode, isAuto) {
7887
8019
  }
7888
8020
  if (mode === "pm") {
7889
8021
  parts.push(...buildPmRelaunchParts(context, lastAgentIdx, isAuto, agentMode));
8022
+ } else if (isPlanningSession(mode, agentMode, isAuto)) {
8023
+ parts.push(
8024
+ scenario === "feedback_relaunch" ? `You have been relaunched with new feedback.` : `You were relaunched but no new instructions have been given since your last run.`,
8025
+ ...newMessagesBlock(allNew.filter((m) => m.role === "user")),
8026
+ ...plannerKickoff(`You are the Planner on this card.`)
8027
+ );
7890
8028
  } else if (scenario === "feedback_relaunch") {
7891
- const newMessages = allNew.filter((m) => m.role === "user");
7892
8029
  parts.push(
7893
8030
  `You have been relaunched with new feedback.`,
7894
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
7895
- `
7896
- New messages since your last run:`,
7897
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
7898
- `
7899
- Address the requested changes. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 review the feedback and implement the changes directly.`,
7900
- `Commit and push your updates.`
8031
+ ...newMessagesBlock(allNew.filter((m) => m.role === "user")),
8032
+ ...builderKickoff(context, `You are the Builder on this card \u2014 address the feedback above.`)
7901
8033
  );
7902
- if (context.githubPRUrl) {
7903
- parts.push(
7904
- `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch. Do NOT create a new PR.`
7905
- );
7906
- } else {
7907
- parts.push(
7908
- `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI.`
7909
- );
7910
- }
7911
8034
  } else {
7912
- parts.push(
7913
- `You were relaunched but no new instructions have been given since your last run.`,
7914
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
7915
- `Run \`git log --oneline -10\` to review what you already committed.`,
7916
- `Review the current state of the codebase and verify everything is working correctly.`
7917
- );
8035
+ parts.push(`You were relaunched but no new instructions have been given since your last run.`);
7918
8036
  if (agentMode === "auto" || agentMode === "building" || isAuto) {
7919
8037
  parts.push(
7920
- `If work is incomplete, continue implementing the plan. When finished, commit, push, and use mcp__conveyor__create_pull_request to open a PR.`,
7921
- `Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
8038
+ ...builderKickoff(context, `You are the Builder on this card \u2014 pick up where you left off.`)
7922
8039
  );
7923
8040
  } else {
7924
8041
  parts.push(
8042
+ branchRule(context),
7925
8043
  `Post a brief status update with post_to_chat (your turn output is NOT shown in chat \u2014 post_to_chat is how the team sees it), then wait for further instructions.`
7926
8044
  );
7927
- }
7928
- if (context.githubPRUrl) {
7929
- parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
8045
+ if (context.githubPRUrl) {
8046
+ parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
8047
+ }
7930
8048
  }
7931
8049
  }
7932
8050
  return parts.join("\n");
@@ -8010,207 +8128,55 @@ ${truncatePlanForPrompt(context.plan)}`);
8010
8128
  }
8011
8129
  }
8012
8130
  if (context.incidents && context.incidents.length > 0) {
8013
- parts.push(...formatIncidents(context.incidents));
8014
- }
8015
- if (context.chatHistory.length > 0) {
8016
- const chatLimit = runnerMode === "task" ? TASK_CHAT_HISTORY_LIMIT : PM_CHAT_HISTORY_LIMIT;
8017
- parts.push(...formatChatHistory(context.chatHistory, chatLimit));
8018
- }
8019
- return parts;
8020
- }
8021
- function buildFreshInstructions(isPm, isAutoMode, context, agentMode) {
8022
- if (isPm && agentMode === "building") {
8023
- return [
8024
- `Your plan has been approved. Begin implementing it now.`,
8025
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
8026
- `Start by reading the relevant source files mentioned in the plan, then write code.`,
8027
- `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI.`,
8028
- `
8029
- CRITICAL: You are in Auto mode. Do NOT report status, ask for confirmation, or go idle without making code changes.`,
8030
- `Your FIRST action must be reading source files from the plan, then immediately writing code.`,
8031
- `Do NOT summarize the plan or say "ready to implement" \u2014 start implementing.`,
8032
- `When all changes are ready, use mcp__conveyor__create_pull_request to open a PR.`,
8033
- `If you are genuinely blocked, explain the specific blocker \u2014 do not go idle silently.`
8034
- ];
8035
- }
8036
- if (isAutoMode && isPm) {
8037
- if (context.plan?.trim()) {
8038
- return [
8039
- `You are operating autonomously. A plan already exists for this task.`,
8040
- `Begin implementing it now \u2014 do NOT re-plan or wait for team input.`,
8041
- `Refine story points, title, and tags via update_task_properties if they look like placeholders.`
8042
- ];
8043
- }
8044
- return [
8045
- `You are operating autonomously. No plan is saved on this card yet.`,
8046
- `1. Search the codebase (grep/glob) to locate relevant files, then read the critical ones`,
8047
- `2. Save a concise plan with update_task_plan BEFORE writing any code \u2014 the plan is a record for the team, not a gate; never pause or wait for approval`,
8048
- `3. Implement the work, keeping the plan current if your approach changes materially`,
8049
- `4. Refine story points, tags, and title (update_task_properties) if they look like placeholders`,
8050
- `Do NOT wait for team input \u2014 proceed autonomously.`
8051
- ];
8052
- }
8053
- if (isPm && context.isParentTask) {
8054
- return [
8055
- `You are the project manager for this task and its subtasks.`,
8056
- `Review existing subtasks via \`list_subtasks\` and the chat history before taking action.`,
8057
- `Read the task description and chat history carefully \u2014 the team has provided the initial context below. Acknowledge what they've asked for and respond in chat before taking silent tool actions.`,
8058
- `Start planning now \u2014 explore the codebase, ask clarifying questions if needed, and propose a subtask breakdown. Do not wait for additional team input before engaging.`,
8059
- `When you finish planning, save the plan with update_task_plan, post a short summary for the team with post_to_chat (your turn output is NOT shown in chat), then end your turn.`
8060
- ];
8061
- }
8062
- if (isPm) {
8063
- return [
8064
- `You are the project manager for this task \u2014 a thoughtful, collaborative planner, not the implementer.`,
8065
- `Your job right now is to help the team turn this request into a clear, well-scoped plan. Be a helpful planning partner: think out loud, surface trade-offs, and keep the human in the loop.`,
8066
- `Read the task description and chat history carefully \u2014 the team has provided the initial context below. Acknowledge what they've asked for and respond in chat before taking silent tool actions.`,
8067
- `Start planning now \u2014 explore the codebase, ask clarifying questions if anything is ambiguous, and draft a plan. Do not wait for additional team input before engaging; the initial message IS the team engaging.`,
8068
- `When you finish planning, save the plan with update_task_plan, post a summary of the plan for the team with post_to_chat (your turn output is NOT shown in chat), then end your turn. A separate task agent will execute the plan after review.`
8069
- ];
8070
- }
8071
- return buildFreshLeafInstructions(context, isAutoMode);
8072
- }
8073
- function buildFreshLeafInstructions(context, isAutoMode) {
8074
- const parts = context.plan?.trim() ? [`Start now by reading the source files the plan names, then writing code.`] : [
8075
- `No plan is saved on this card yet. Investigate the task briefly (search first, read only critical files), then save a concise implementation plan with update_task_plan (file:line citations) BEFORE you start writing code.`,
8076
- `The plan is a step, not the goal: the moment it's posted, IMPLEMENT it in code \u2014 do NOT pause, wait for approval, or treat the posted plan as the deliverable. The card is already In Progress and advances automatically.`
8077
- ];
8078
- const base = context.baseBranch?.trim() || "dev";
8079
- parts.push(
8080
- `Post to chat when you begin implementing and again when the PR is ready.`,
8081
- `Follow the **Pre-PR Protocol** in your system prompt: commit, then sync the base (\`git fetch origin ${base} && git merge origin/${base} --no-edit\`), then ONE verification pass \u2014 \`bun run check\` (lint + typecheck) plus \`bun run test:affected\` (tests scoped to your diff; docs-only changes need no local gates) \u2014 then open the PR. Do NOT re-merge the base or re-run a gate that already passed.`,
8082
- `If a gate fails, iterate on just the failing test files (\`bun run --cwd <pkg> test:unit <files>\`) and confirm with a single final scoped-gate run. Do NOT open a PR with known failing gates.`,
8083
- `Open the PR with mcp__conveyor__create_pull_request when the work is done and the gates are green.`
8084
- );
8085
- if (isAutoMode) {
8086
- parts.push(
8087
- `
8088
- CRITICAL: You are in Auto mode. Your job is to BUILD the change, not to produce a plan \u2014 making a plan and opening a PR of the plan is NOT the goal. The plan is only an intermediate step; you must then write the code that implements it.`,
8089
- `Do NOT report status, ask for confirmation, or go idle without making code changes. Do NOT summarize the plan or say "ready to implement" \u2014 start implementing immediately.`,
8090
- `Your pull request MUST contain the actual code implementation. Never open a plan-only or empty-diff PR. (If the task genuinely needs no code changes, do not open a PR \u2014 deliver the result in chat and mark the card Complete.)`,
8091
- `When the implementation is complete and verified, you MUST use mcp__conveyor__create_pull_request to open a PR before finishing.`,
8092
- `If you are genuinely blocked, explain the specific blocker \u2014 do not go idle silently.`
8093
- );
8094
- }
8095
- return parts;
8096
- }
8097
- function buildFreshCodeReviewInstructions(context) {
8098
- const parts = [
8099
- `Perform the automated code review for this PR.`,
8100
- `Work on the git branch "${context.githubBranch}". Stay on this branch for the entire review. Do not checkout or create other branches.`,
8101
- `Start by establishing what you are actually reviewing${context.githubPRUrl ? `: \`gh pr view ${context.githubPRUrl} --json state,baseRefName,headRefOid,files\`` : ` \u2014 the PR's own base ref, head SHA and changed-file list`}. Do this BEFORE any git diff: the PR's base is often not the project default branch, and asking the PR costs one call instead of six.`,
8102
- `Then inspect the submitted changes with \`${baseDiffCommand(context.baseBranch)}\`. If that disagrees with the PR's file list, trust the PR: diff the head SHA directly (\`git diff <headSha>^ <headSha>\`) and review that.`,
8103
- `Your checkout can be stale, and so can the remote branch tip \u2014 a merged PR's commit may be reachable only by SHA. If the PR reports state MERGED or CLOSED, say so and review the recorded head SHA rather than assuming your working tree matches it.`,
8104
- `Read the code under review out of the PR's commit, NOT the working tree: \`git show <sha>:<path>\` and \`git grep <pattern> <sha>\`. Grepping the working tree when the PR is based elsewhere returns confident, wrong answers \u2014 it has produced false review findings.`,
8105
- `Review for correctness, security, performance, error handling, test coverage, and consistency with existing patterns.`,
8106
- `Consult the card's tag Reference Guides in the brief above for the domain rules that apply to this diff \u2014 they are the conventions the change must follow, and a contradiction of them is a review finding.`,
8107
- `If small fixes are needed, make them directly, commit, push, and re-review the result.`,
8108
- `Use approve_code_review when the PR passes review.`,
8109
- `Use request_code_changes when substantive issues remain that you cannot fix directly.`
8110
- ];
8111
- if (context.githubPRUrl) {
8112
- parts.push(`The PR under review is ${context.githubPRUrl}. Do not create a new PR.`);
8113
- }
8114
- return parts;
8115
- }
8116
- function buildFeedbackInstructions(context, isPm, agentMode, isAuto) {
8117
- const lastAgentIdx = findLastAgentMessageIndex(context.chatHistory);
8118
- const newMessages = context.chatHistory.slice(lastAgentIdx + 1).filter((m) => m.role === "user");
8119
- if (isPm) {
8120
- const parts2 = [
8121
- `You were relaunched with new feedback since your last run.`,
8122
- `You are the project manager for this task.`,
8123
- `
8124
- New messages since your last run:`,
8125
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`)
8126
- ];
8127
- if (isAuto && (agentMode === "building" || agentMode === "review")) {
8128
- parts2.push(
8129
- `
8130
- Your plan has been approved. Address the feedback above, then begin implementing.`,
8131
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
8132
- `Start by reading the relevant source files mentioned in the plan, then write code.`,
8133
- `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI.`
8134
- );
8135
- } else if (isAuto) {
8136
- parts2.push(
8137
- `
8138
- You are in auto mode. Address the feedback above and continue building \u2014 update the saved plan with update_task_plan if the feedback changes your approach.`,
8139
- `Do NOT wait for additional team input \u2014 the messages above ARE the team's input. Proceed autonomously.`
8140
- );
8141
- } else {
8142
- parts2.push(
8143
- `
8144
- Review these messages and wait for the team to provide instructions before taking action.`
8145
- );
8146
- }
8147
- return parts2;
8148
- }
8149
- const parts = [
8150
- `You have been relaunched to address feedback on your previous work.`,
8151
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
8152
- `Start by running \`${baseDiffCommand(context.baseBranch, "--stat")}\` to review what you already committed on this branch.`,
8153
- `
8154
- New messages since your last run:`,
8155
- ...newMessages.map((m) => `[${m.userName ?? "user"}]: ${m.content}`),
8156
- `
8157
- Address the requested changes directly. Do NOT re-investigate the codebase from scratch or write a new plan \u2014 go straight to implementing the feedback.`,
8158
- // Scoped to what the follow-up actually changed. An unconditional full
8159
- // re-verify on every relaunch made a one-line or docs-only follow-up cost
8160
- // the same gate time as the original build.
8161
- `Re-verify in proportion to what THIS follow-up changed, not the whole branch: a docs/\`.claude\`-only change needs no local gates; a code change runs \`bun run check\` plus the directly-affected test files (\`bun run --cwd <pkg> test:unit <files>\`); escalate to a full \`bun run test:affected\` only when the follow-up touched \`packages/shared\`, \`packages/db\`, or root config, or when no gate has ever run on this branch. Fix any failures before pushing \u2014 relaunches are the most common place verification gets skipped.`,
8162
- `Implement your updates and open a PR when finished.`
8163
- ];
8131
+ parts.push(...formatIncidents(context.incidents));
8132
+ }
8133
+ const undelivered = relaunchMessageBatch(context);
8134
+ if (undelivered.length > 0) {
8135
+ const chatLimit = runnerMode === "task" ? TASK_CHAT_HISTORY_LIMIT : PM_CHAT_HISTORY_LIMIT;
8136
+ parts.push(...formatChatHistory(undelivered, chatLimit));
8137
+ }
8138
+ return parts;
8139
+ }
8140
+ function buildFreshInstructions(isPlanning, context) {
8141
+ return isPlanning ? plannerKickoff(
8142
+ `You are the Planner on this card \u2014 scope and plan it with the team. A separate Builder session implements it.`
8143
+ ) : builderKickoff(context, `You are the Builder on this card \u2014 implement its plan.`);
8144
+ }
8145
+ function buildFreshCodeReviewInstructions(context) {
8146
+ const parts = [...buildSkillInvocation("/conveyor-review"), branchRule(context, true)];
8164
8147
  if (context.githubPRUrl) {
8165
- parts.push(
8166
- `An existing PR is open at ${context.githubPRUrl} \u2014 push to the same branch to update it. Do NOT create a new PR.`
8167
- );
8168
- } else {
8169
- parts.push(
8170
- `When finished, use the mcp__conveyor__create_pull_request tool to open a PR. Do NOT use gh CLI or any other method to create PRs.`
8171
- );
8148
+ parts.push(`The PR under review is ${context.githubPRUrl}. Do not create a new PR.`);
8172
8149
  }
8173
8150
  return parts;
8174
8151
  }
8175
- function buildIdleRelaunchInstructions(context, isPm, agentMode, isAuto) {
8176
- if (isPm && agentMode === "auto" && PRE_BUILD_TASK_STATUSES.has(context.status ?? "")) {
8177
- if (context.plan?.trim()) {
8178
- return [
8179
- `You were relaunched in auto mode. A plan already exists for this task.`,
8180
- `Begin implementing it now \u2014 refine story points, title, and tags via update_task_properties if they look like placeholders.`,
8181
- `Do NOT wait for instructions or go idle \u2014 you are in auto mode.`
8182
- ];
8183
- }
8152
+ function buildFeedbackInstructions(context, isPlanning) {
8153
+ return isPlanning ? plannerKickoff(
8154
+ `You have been relaunched with the feedback in New Messages above. You are the Planner on this card \u2014 fold it into the plan.`
8155
+ ) : builderKickoff(
8156
+ context,
8157
+ `You have been relaunched to address the feedback in New Messages above.`
8158
+ );
8159
+ }
8160
+ function buildIdleRelaunchInstructions(context, isPlanning, agentMode, isAuto) {
8161
+ const opener = `You were relaunched but no new instructions have been given since your last run.`;
8162
+ if (isPlanning) {
8184
8163
  return [
8185
- `You were relaunched in auto mode. Continue building autonomously.`,
8186
- `No plan is saved on this card yet \u2014 save a concise plan with update_task_plan before writing further code; never pause or wait for approval.`,
8187
- `Do NOT wait for instructions or go idle \u2014 you are in auto mode.`
8164
+ opener,
8165
+ `You are the Planner on this card. Post a brief status update with post_to_chat (your turn output is NOT shown in chat \u2014 post_to_chat is how the team sees it), then wait for the team.`
8188
8166
  ];
8189
8167
  }
8190
- if (isPm && !(agentMode === "building" || agentMode === "review" || agentMode === "auto")) {
8168
+ if (agentMode === "auto" || agentMode === "building" || isAuto) {
8191
8169
  return [
8192
- `You were relaunched but no new instructions have been given since your last run.`,
8193
- `You are the project manager for this task.`,
8194
- `Wait for the team to provide instructions before taking action.`
8170
+ opener,
8171
+ ...builderKickoff(context, `You are the Builder on this card \u2014 pick up where you left off.`)
8195
8172
  ];
8196
8173
  }
8197
- const isAutoMode = agentMode === "auto" || agentMode === "building" || isAuto;
8198
8174
  const parts = [
8199
- `You were relaunched but no new instructions have been given since your last run.`,
8200
- `Work on the git branch "${context.githubBranch}". Stay on this branch \u2014 do not checkout or create other branches.`,
8201
- `Run \`git log --oneline -10\` to review what you already committed, then verify the current state is correct.`
8175
+ opener,
8176
+ branchRule(context),
8177
+ `Post a brief status update summarizing where things stand with post_to_chat (your turn output is NOT shown in chat \u2014 post_to_chat is how the team sees it).`,
8178
+ `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
8202
8179
  ];
8203
- if (isAutoMode) {
8204
- parts.push(
8205
- `If work is incomplete, continue implementing the plan. When finished, use mcp__conveyor__create_pull_request to open a PR.`,
8206
- `Do NOT go idle or wait for instructions \u2014 you are in auto mode.`
8207
- );
8208
- } else {
8209
- parts.push(
8210
- `Post a brief status update summarizing where things stand with post_to_chat (your turn output is NOT shown in chat \u2014 post_to_chat is how the team sees it).`,
8211
- `Then wait for further instructions \u2014 do NOT redo work that was already completed.`
8212
- );
8213
- }
8214
8180
  if (context.githubPRUrl) {
8215
8181
  parts.push(`An existing PR is open at ${context.githubPRUrl}. Do not create a new PR.`);
8216
8182
  }
@@ -8221,6 +8187,7 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
8221
8187
  ## Instructions`];
8222
8188
  const isPm = mode === "pm";
8223
8189
  const isCodeReview = mode === "code-review" || !isPm && agentMode === "review";
8190
+ const isPlanning = isPlanningSession(mode, agentMode, isAuto);
8224
8191
  if (agentMode === "chat") {
8225
8192
  parts.push(...buildChatInstructions(context, scenario, relaunchMessageBatch(context)));
8226
8193
  return parts;
@@ -8230,9 +8197,7 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
8230
8197
  return parts;
8231
8198
  }
8232
8199
  if (scenario === "fresh") {
8233
- parts.push(
8234
- ...buildFreshInstructions(isPm, agentMode === "auto" || !!isAuto, context, agentMode)
8235
- );
8200
+ parts.push(...buildFreshInstructions(isPlanning, context));
8236
8201
  return parts;
8237
8202
  }
8238
8203
  const newBatch = relaunchMessageBatch(context);
@@ -8246,13 +8211,13 @@ function buildInstructions(mode, context, scenario, agentMode, isAuto) {
8246
8211
  return parts;
8247
8212
  }
8248
8213
  if (scenario === "idle_relaunch") {
8249
- parts.push(...buildIdleRelaunchInstructions(context, isPm, agentMode, isAuto));
8214
+ parts.push(...buildIdleRelaunchInstructions(context, isPlanning, agentMode, isAuto));
8250
8215
  return parts;
8251
8216
  }
8252
- parts.push(...buildFeedbackInstructions(context, isPm, agentMode, isAuto));
8217
+ parts.push(...buildFeedbackInstructions(context, isPlanning));
8253
8218
  return parts;
8254
8219
  }
8255
- async function buildInitialPrompt(mode, context, isAuto, agentMode) {
8220
+ async function buildInitialPrompt(mode, context, isAuto, agentMode, packExecution) {
8256
8221
  const isPackRunner = mode === "pack" || mode === "pm" && !!isAuto && !!context.isParentTask;
8257
8222
  if (!isPackRunner) {
8258
8223
  const sessionRelaunch = buildRelaunchWithSession(mode, context, agentMode, isAuto);
@@ -8266,12 +8231,13 @@ async function buildInitialPrompt(mode, context, isAuto, agentMode) {
8266
8231
  scenario = "fresh";
8267
8232
  }
8268
8233
  const body = await buildTaskBody(context, mode);
8269
- const instructions = isPackRunner ? buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode, isAuto);
8234
+ const singlePodPack = mode === "pack" && packExecution !== "fan-out";
8235
+ const instructions = isPackRunner ? singlePodPack ? buildSinglePodPackInstructions(context, scenario) : buildPackRunnerInstructions(context, scenario) : buildInstructions(mode, context, scenario, agentMode, isAuto);
8270
8236
  return [...body, ...instructions].join("\n");
8271
8237
  }
8272
8238
 
8273
8239
  // src/tools/task-context-tools.ts
8274
- import { z as z11 } from "zod";
8240
+ import { z as z13 } from "zod";
8275
8241
 
8276
8242
  // ../shared/dist/tool-contracts/index.js
8277
8243
  var f = {
@@ -8300,14 +8266,14 @@ var f = {
8300
8266
  return { kind: "nullable", inner };
8301
8267
  }
8302
8268
  };
8303
- function compileString(z19, spec) {
8304
- let schema = z19.string();
8269
+ function compileString(z20, spec) {
8270
+ let schema = z20.string();
8305
8271
  if (spec.min !== void 0) schema = schema.min(spec.min);
8306
8272
  if (spec.max !== void 0) schema = schema.max(spec.max);
8307
8273
  return schema;
8308
8274
  }
8309
- function compileNumber(z19, spec) {
8310
- let schema = z19.number();
8275
+ function compileNumber(z20, spec) {
8276
+ let schema = z20.number();
8311
8277
  if (spec.int) schema = schema.int();
8312
8278
  if (spec.positive) schema = schema.positive();
8313
8279
  if (spec.nonnegative) schema = schema.nonnegative();
@@ -8315,41 +8281,49 @@ function compileNumber(z19, spec) {
8315
8281
  if (spec.max !== void 0) schema = schema.max(spec.max);
8316
8282
  return schema;
8317
8283
  }
8318
- function compileArray(z19, spec) {
8319
- let schema = z19.array(compileField(z19, spec.item));
8284
+ function compileArray(z20, spec) {
8285
+ let schema = z20.array(compileField(z20, spec.item));
8320
8286
  if (spec.min !== void 0) schema = schema.min(spec.min);
8321
8287
  return schema;
8322
8288
  }
8323
- function compileBase(z19, spec) {
8289
+ function compileBase(z20, spec) {
8324
8290
  switch (spec.kind) {
8325
8291
  case "string":
8326
- return compileString(z19, spec);
8292
+ return compileString(z20, spec);
8327
8293
  case "number":
8328
- return compileNumber(z19, spec);
8294
+ return compileNumber(z20, spec);
8329
8295
  case "boolean":
8330
- return z19.boolean();
8296
+ return z20.boolean();
8331
8297
  case "enum":
8332
- return z19.enum([...spec.values]);
8298
+ return z20.enum([...spec.values]);
8333
8299
  case "array":
8334
- return compileArray(z19, spec);
8300
+ return compileArray(z20, spec);
8335
8301
  case "object":
8336
- return z19.object(compileShape(z19, spec.fields));
8302
+ return z20.object(compileShape(z20, spec.fields));
8337
8303
  }
8338
8304
  }
8339
- function compileField(z19, spec) {
8305
+ function descriptionOf(spec) {
8306
+ if (spec.kind === "optional" || spec.kind === "nullable") return descriptionOf(spec.inner);
8307
+ return spec.desc;
8308
+ }
8309
+ function compileUndescribed(z20, spec) {
8340
8310
  if (spec.kind === "optional") {
8341
- return compileField(z19, spec.inner).optional();
8311
+ return compileUndescribed(z20, spec.inner).optional();
8342
8312
  }
8343
8313
  if (spec.kind === "nullable") {
8344
- return compileField(z19, spec.inner).nullable();
8314
+ return compileUndescribed(z20, spec.inner).nullable();
8345
8315
  }
8346
- const schema = compileBase(z19, spec);
8347
- return spec.desc === void 0 ? schema : schema.describe(spec.desc);
8316
+ return compileBase(z20, spec);
8317
+ }
8318
+ function compileField(z20, spec) {
8319
+ const schema = compileUndescribed(z20, spec);
8320
+ const desc = descriptionOf(spec);
8321
+ return desc === void 0 ? schema : schema.describe(desc);
8348
8322
  }
8349
- function compileShape(z19, fields) {
8323
+ function compileShape(z20, fields) {
8350
8324
  const shape = {};
8351
8325
  for (const [key, spec] of Object.entries(fields)) {
8352
- shape[key] = compileField(z19, spec);
8326
+ shape[key] = compileField(z20, spec);
8353
8327
  }
8354
8328
  return shape;
8355
8329
  }
@@ -8537,14 +8511,118 @@ var approveAndMergePrContract = defineToolContract({
8537
8511
  }
8538
8512
  }
8539
8513
  });
8514
+ var getConnectionContextContract = defineToolContract({
8515
+ name: "get_connection_context",
8516
+ agent: {
8517
+ description: "Resolve WHAT this session is bound to \u2014 call this FIRST, before inferring anything from names or branch strings. Returns the current task (id, title, status, branch, baseBranch, whether it is a pack parent, and its parentTaskId when it is itself a child), its project (id, name), and the acting agent (id, model, mode, isAuto). This is the pod-side half of the same tool the external MCP surface exposes, so a shared skill can open with one call in both places; it reports no user account, because a pod is bound to a card rather than to a person's session.",
8518
+ fields: {}
8519
+ },
8520
+ mcp: {
8521
+ description: "Resolve WHO this connection is and WHAT project/board it points at \u2014 call this FIRST, before inferring anything from names. Returns the effective account, project, and board (sub-project) each with BOTH its immutable ID and its human-readable name/slug, the granted capabilities (read/create/update/chat/files/build), management URLs, and a one-line summary. Removes the ambiguity between a project's canonical name and a board label. Pass projectId to target a specific project; otherwise the configured default project is used.",
8522
+ fields: {
8523
+ projectId: mcpProjectId
8524
+ }
8525
+ }
8526
+ });
8540
8527
  var tasksContracts = [
8541
8528
  getTaskContract,
8542
8529
  postToChatContract,
8543
8530
  readTaskChatContract,
8544
8531
  listTagsContract,
8545
8532
  searchTasksContract,
8546
- approveAndMergePrContract
8533
+ approveAndMergePrContract,
8534
+ getConnectionContextContract
8535
+ ];
8536
+ var STATUS_ENUM = [
8537
+ "Planning",
8538
+ "Open",
8539
+ "InProgress",
8540
+ "ReviewPR",
8541
+ "ReviewDev",
8542
+ "ReviewLive",
8543
+ "Complete",
8544
+ "Cancelled"
8547
8545
  ];
8546
+ var RISK_ENUM = ["critical", "high", "medium", "low"];
8547
+ var GITHUB_BRANCH_MEANING = "Record the task's ACTUAL working branch (e.g. a locally-driven pack's branch cut at claim time, so identification never mints a competing name and pack-child merges settle against reality).";
8548
+ var GITHUB_BRANCH_DESC = "Record the task's ACTUAL working branch (e.g. a locally-driven pack's branch cut at claim time, so identification never mints a competing name and pack-child merges settle against reality), or null to detach. The branch must already exist on origin, and a live workspace bound to a different branch rejects the write.";
8549
+ var updateTaskContract = defineToolContract({
8550
+ name: "update_task",
8551
+ agent: {
8552
+ description: "Update the CURRENT task's title, description, plan, githubBranch, or status \u2014 the one tool the shared Conveyor skills use for card writes, so the same skill text works here and in a local MCP session. Pass task_id ONLY with status, to move one of this card's children (claim it InProgress, promote it to Open); every other field always applies to the current task, because the server scopes plan/title writes to this session. Status rides the same path a human or a build uses, so watchdog and board semantics hold \u2014 for an emergency override that skips those, use force_update_task_status.",
8553
+ fields: {
8554
+ title: f.optional(f.string({ desc: "New title" })),
8555
+ description: f.optional(f.string({ desc: cardDescriptionDesc("New description") })),
8556
+ plan: f.optional(f.string({ desc: "New plan (markdown)" })),
8557
+ status: f.optional(f.enum(STATUS_ENUM, { desc: "New status" })),
8558
+ // Not nullable here, unlike the mcp surface: the pod's underlying
8559
+ // `updateTaskProperties` takes `githubBranch?: string` with no null
8560
+ // branch, so a pod can RECORD a branch but cannot detach one. Offering
8561
+ // null would advertise a write the server would silently drop.
8562
+ githubBranch: f.optional(
8563
+ f.string({
8564
+ min: 1,
8565
+ desc: `${GITHUB_BRANCH_MEANING} The pod does NOT validate this: the value is written through with no check that the ref exists on origin, and no guard against repointing the record away from the branch this pod is actually running on (which strands the compute). Push the branch first, and only name a branch this session genuinely owns. Detaching (clearing) a branch is not available from a pod \u2014 use the external MCP surface for that.`
8566
+ })
8567
+ ),
8568
+ task_id: f.optional(
8569
+ f.string({
8570
+ // min:1 is load-bearing: "" is falsy, so an empty task_id would slip
8571
+ // past both the refusal below and the child-routing branch, and
8572
+ // silently update the CURRENT card — the exact mis-targeting this
8573
+ // field's guard exists to prevent.
8574
+ min: 1,
8575
+ desc: "Child task ID to move. Valid ONLY alongside status \u2014 pairing it with title/description/plan/githubBranch is rejected rather than silently applied to the current card. Omit to update the current task."
8576
+ })
8577
+ )
8578
+ }
8579
+ },
8580
+ mcp: {
8581
+ description: "Update task fields: title, description, plan, status, risk, story points, assignment, githubBranch, or tags. Set status to claim a card (InProgress), triage it (Open), or cancel it (Cancelled); for review approvals prefer approve_task / request_changes, which guard against stale-state races. Tags are additive/subtractive \u2014 pass addTags/removeTags with tag names (not a replace-set). Pass projectId to target a specific project; otherwise the configured default project is used. Moving a task beyond Planning auto-fills any missing icon, story points, and agent assignment \u2014 don't spend turns on them; pass storyPointValue only to correct the sizing yourself. For subtasks use update_subtask.",
8582
+ fields: {
8583
+ projectId: mcpProjectId,
8584
+ taskId: f.string({ desc: "The task ID" }),
8585
+ title: f.optional(f.string({ desc: "New title" })),
8586
+ description: f.optional(f.string({ desc: cardDescriptionDesc("New description") })),
8587
+ plan: f.optional(f.string({ desc: "New plan (markdown)" })),
8588
+ status: f.optional(f.enum(STATUS_ENUM, { desc: "New status" })),
8589
+ risk: f.optional(
8590
+ f.nullable(
8591
+ f.enum(RISK_ENUM, {
8592
+ desc: "Risk level \u2014 how much important surface the task touches (critical/high/medium/low). Pass null to clear."
8593
+ })
8594
+ )
8595
+ ),
8596
+ storyPointValue: f.optional(
8597
+ f.nullable(
8598
+ f.number({
8599
+ desc: `${storyPointValueDesc}. The tiers are per-project \u2014 a value the project has not configured is rejected. Pass null to clear it; a card beyond Planning with no story point is re-filled by identification.`
8600
+ })
8601
+ )
8602
+ ),
8603
+ assignedUserId: f.optional(f.nullable(f.string({ desc: "User ID to assign, or null" }))),
8604
+ subProjectId: f.optional(
8605
+ f.nullable(
8606
+ f.string({
8607
+ desc: "Assign the task to a sub-project board; null moves it back to the parent board. Omit to leave unchanged."
8608
+ })
8609
+ )
8610
+ ),
8611
+ githubBranch: f.optional(f.nullable(f.string({ desc: GITHUB_BRANCH_DESC }))),
8612
+ addTags: f.optional(
8613
+ f.array(f.string(), {
8614
+ desc: 'Tag names to add to the card (e.g. ["refactor"]). Additive \u2014 existing tags are kept. Unknown names are rejected; use list_tags to see available tags or manage_tags to create one.'
8615
+ })
8616
+ ),
8617
+ removeTags: f.optional(
8618
+ f.array(f.string(), {
8619
+ desc: "Tag names to remove from the card. Removing a tag the card doesn't have is a no-op."
8620
+ })
8621
+ )
8622
+ }
8623
+ }
8624
+ });
8625
+ var taskUpdateContracts = [updateTaskContract];
8548
8626
  var tagRef = f.string({
8549
8627
  desc: "Tag id, or the exact tag name (case-insensitive)",
8550
8628
  min: 1,
@@ -8835,7 +8913,7 @@ var createSubtaskContract = defineToolContract({
8835
8913
  var updateSubtaskContract = defineToolContract({
8836
8914
  name: "update_subtask",
8837
8915
  agent: {
8838
- description: "Update an existing subtask's fields (title, description, plan, ordinal, storyPointValue, dependsOn) \u2014 and the sanctioned path to make a child buildable: promote it to status Open, assign its agent (agentIdOrName), and set story points. Setting story points does NOT auto-promote; set status explicitly. For the current task use update_task_plan.",
8916
+ description: "Update an existing subtask's fields (title, description, plan, ordinal, storyPointValue, dependsOn) \u2014 and the sanctioned path to make a child buildable: promote it to status Open, assign its agent (agentIdOrName), and set story points. Setting story points does NOT auto-promote; set status explicitly. For the current task use update_task.",
8839
8917
  fields: {
8840
8918
  subtaskId: f.string({ desc: "The subtask ID to update" }),
8841
8919
  title: f.optional(f.string()),
@@ -8900,7 +8978,7 @@ var deleteSubtaskContract = defineToolContract({
8900
8978
  var listSubtasksContract = defineToolContract({
8901
8979
  name: "list_subtasks",
8902
8980
  agent: {
8903
- description: "List all subtasks under the current parent task. Default compact view returns per child: status, agent, story points, PR number/state, dependencies, and holdsBuildSlot \u2014 plus packSlots (in-flight environments vs the PACK_CHILD_LIMIT cap, and which children hold the slots). Use to coordinate child work; pass verbose:true only when you need full description/plan text (large). For non-child tasks use get_task.",
8981
+ description: "List all subtasks under the current parent task. Default compact view returns per child: status, agent, story points, PR number/state, and dependencies. On a FAN-OUT pack it also returns holdsBuildSlot per child plus a packSlots summary (in-flight environments vs the PACK_CHILD_LIMIT cap, and which children hold the slots); both are omitted on the default single-pod path, where one pod implements every child and nothing holds a slot. Use to coordinate child work; pass verbose:true only when you need full description/plan text (large). For non-child tasks use get_task.",
8904
8982
  fields: {
8905
8983
  verbose: f.optional(
8906
8984
  f.boolean({
@@ -9103,41 +9181,448 @@ var createPullRequestContract = defineToolContract({
9103
9181
  }
9104
9182
  },
9105
9183
  mcp: {
9106
- description: "Open a GitHub pull request for a task's existing branch (the branch must already be pushed to origin). Pass projectId to target a specific project; otherwise the configured default project is used. Moves the task to ReviewPR. Returns the PR number and URL.",
9107
- fields: {
9108
- projectId: mcpProjectId,
9109
- taskId: f.string({ desc: "The task ID whose branch should be opened as a PR" }),
9110
- title: f.string({ desc: "Pull request title" }),
9111
- body: f.string({
9112
- desc: "Pull request body (markdown). For a diff that changes rendered UI, embed the visual proof inline: upload the capture with upload_attachment and paste the downloadUrl it returns as ![caption](url), in addition to leaving it on the card."
9113
- }),
9114
- head: f.optional(
9115
- f.string({ desc: "Source branch for the PR (defaults to the task's branch)" })
9116
- ),
9117
- base: f.optional(
9118
- f.string({ desc: "Target branch for the PR (defaults to the repo default)" })
9119
- )
9120
- }
9184
+ description: "Open a GitHub pull request for a task's existing branch (the branch must already be pushed to origin). Pass projectId to target a specific project; otherwise the configured default project is used. Moves the task to ReviewPR. Returns the PR number and URL.",
9185
+ fields: {
9186
+ projectId: mcpProjectId,
9187
+ taskId: f.string({ desc: "The task ID whose branch should be opened as a PR" }),
9188
+ title: f.string({ desc: "Pull request title" }),
9189
+ body: f.string({
9190
+ desc: "Pull request body (markdown). For a diff that changes rendered UI, embed the visual proof inline: upload the capture with upload_attachment and paste the downloadUrl it returns as ![caption](url), in addition to leaving it on the card."
9191
+ }),
9192
+ head: f.optional(
9193
+ f.string({ desc: "Source branch for the PR (defaults to the task's branch)" })
9194
+ ),
9195
+ base: f.optional(
9196
+ f.string({ desc: "Target branch for the PR (defaults to the repo default)" })
9197
+ )
9198
+ }
9199
+ }
9200
+ });
9201
+ var pullRequestContracts = [createPullRequestContract];
9202
+ var REGISTRY_RULE = "Only channels an admin registered as work channels in project settings are reachable \u2014 an unregistered channel is invisible here no matter what the bot can see in the workspace, and the card-sync channels are NOT automatically included.";
9203
+ var NO_STORAGE = "Messages are fetched live from Slack/Discord on every call and are never stored by Conveyor.";
9204
+ var listProjectIntegrationsContract = defineToolContract({
9205
+ name: "list_project_integrations",
9206
+ agent: {
9207
+ description: `Find out what this project is connected to before assuming a capability exists \u2014 repository forge, Slack or Discord, email, GCP, Grafana, Google Analytics, Google Drive, Cloudflare, and whether incident reporting is set up. Also lists the registered work channels you may read or post in, and how many meetings have been ingested \u2014 so you can tell whether reading meetings is worth doing before calling a meeting tool. Credential-free: every value says whether something is configured, never what the secret is. Call this when you are about to reach for an integration and want to know whether it is there, rather than trying and handling a failure.`,
9208
+ fields: {}
9209
+ },
9210
+ mcp: {
9211
+ description: `Report which integrations a project has configured \u2014 repository forge, Slack/Discord, email, GCP, Grafana, Google Analytics, Google Drive, Cloudflare, incident reporting \u2014 plus the work channels registered for agent access and the project's ingested-meeting count. Returns configuration booleans only, never credential material. Pass projectId to target a specific project; otherwise the configured default project is used.`,
9212
+ fields: {
9213
+ projectId: mcpProjectId
9214
+ }
9215
+ }
9216
+ });
9217
+ var listProjectChannelsContract = defineToolContract({
9218
+ name: "list_project_channels",
9219
+ agent: {
9220
+ description: `List the Slack or Discord channels this project has registered as work channels, each with the admin's description of what happens there and whether you may read it and post into it. Start here before read_channel_messages \u2014 the description is what tells you which channel is worth consulting. ${REGISTRY_RULE}`,
9221
+ fields: {}
9222
+ },
9223
+ mcp: {
9224
+ description: `List a project's registered work channels: provider, channel id and name, the admin's description of what the channel is for, and the allowRead/allowPost grants. ${REGISTRY_RULE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
9225
+ fields: {
9226
+ projectId: mcpProjectId
9227
+ }
9228
+ }
9229
+ });
9230
+ var CHANNEL_ID_DESC = "Provider channel id, exactly as list_project_channels reports it (Slack `C\u2026`, Discord a numeric id). Not the channel name.";
9231
+ var LIMIT_DESC = "How many messages to return, newest first. Defaults to 25, maximum 50. Prefer a small read plus a follow-up over one large one \u2014 channel history spends context fast.";
9232
+ var BEFORE_DESC = "Page further back: return messages older than this cursor. Pass the `olderCursor` from a previous call; a null cursor there means you have reached the start of the channel.";
9233
+ var AFTER_DESC = "Return only messages newer than this cursor. Use to catch up on a channel.";
9234
+ var THREAD_DESC = "Read one thread instead of the channel surface. Pass a `threadTs` seen on a message from a previous read.";
9235
+ var readChannelMessagesContract = defineToolContract({
9236
+ name: "read_channel_messages",
9237
+ agent: {
9238
+ description: `Read recent messages from a registered work channel, newest first \u2014 to catch up on a discussion, find the context behind a decision, or check whether something was already raised. Each message reports its author, whether that author is a bot (Conveyor's own card feed posts show up here too, so check this before treating a message as a teammate's), its text, and a cursor for paging further back. ${REGISTRY_RULE} ${NO_STORAGE}`,
9239
+ fields: {
9240
+ channelId: f.string({ desc: CHANNEL_ID_DESC }),
9241
+ limit: f.optional(f.number({ desc: LIMIT_DESC, int: true, min: 1, max: 50 })),
9242
+ before: f.optional(f.string({ desc: BEFORE_DESC })),
9243
+ after: f.optional(f.string({ desc: AFTER_DESC })),
9244
+ threadTs: f.optional(f.string({ desc: THREAD_DESC }))
9245
+ }
9246
+ },
9247
+ mcp: {
9248
+ description: `Read recent messages from a project's registered work channel, newest first. Each message carries author name, a bot flag, text, a provider-native cursor, and a timestamp; the response carries an olderCursor for paging back. ${REGISTRY_RULE} ${NO_STORAGE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
9249
+ fields: {
9250
+ projectId: mcpProjectId,
9251
+ channelId: f.string({ desc: CHANNEL_ID_DESC }),
9252
+ limit: f.optional(f.number({ desc: LIMIT_DESC, int: true, min: 1, max: 50 })),
9253
+ before: f.optional(f.string({ desc: BEFORE_DESC })),
9254
+ after: f.optional(f.string({ desc: AFTER_DESC })),
9255
+ threadTs: f.optional(f.string({ desc: THREAD_DESC }))
9256
+ }
9257
+ }
9258
+ });
9259
+ var POST_TEXT_DESC = "What to say, in plain text or light markdown. An attribution footer naming you is appended automatically, so do not sign the message yourself. Channel-wide pings (@everyone, @here, <!channel>) are rendered as ordinary text and will not notify anyone.";
9260
+ var POST_THREAD_DESC = "Reply inside a thread rather than to the channel. Prefer this when responding to a specific message \u2014 pass the `threadTs` from the message you are answering.";
9261
+ var POST_WARNING = "This is visible to everyone in the channel and cannot be edited or deleted afterwards. Posting requires a per-channel grant that is OFF by default, separate from read access, so most registered channels will refuse it.";
9262
+ var postChannelMessageContract = defineToolContract({
9263
+ name: "post_channel_message",
9264
+ agent: {
9265
+ description: `Post a message into a registered work channel \u2014 to answer a question aimed at you, report something the team is waiting on, or ask for input you genuinely need. ${POST_WARNING} ${REGISTRY_RULE}`,
9266
+ fields: {
9267
+ channelId: f.string({ desc: CHANNEL_ID_DESC }),
9268
+ text: f.string({ desc: POST_TEXT_DESC, min: 1, max: 1800 }),
9269
+ threadTs: f.optional(f.string({ desc: POST_THREAD_DESC }))
9270
+ }
9271
+ },
9272
+ mcp: {
9273
+ description: `Post a message into a project's registered work channel, attributed to the acting agent or user. ${POST_WARNING} ${REGISTRY_RULE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
9274
+ fields: {
9275
+ projectId: mcpProjectId,
9276
+ channelId: f.string({ desc: CHANNEL_ID_DESC }),
9277
+ text: f.string({ desc: POST_TEXT_DESC, min: 1, max: 1800 }),
9278
+ threadTs: f.optional(f.string({ desc: POST_THREAD_DESC }))
9279
+ }
9280
+ }
9281
+ });
9282
+ var ANALYTICS_RANGE = "How many days back to summarize, ending today. Defaults to 28; 90 is the maximum, because GA4 retention makes longer windows unreliable.";
9283
+ var ANALYTICS_CAMPAIGN = "Restrict to one campaign name. The campaigns breakdown is always unfiltered, so read it first to see what exists.";
9284
+ var ANALYTICS_SHAPE = "Returns headline totals (sessions, active users, new users, page views, average session duration, bounce rate) plus the top five pages, traffic sources, landing pages and campaigns. A project that has not set up GA4 answers configured:false with a reason rather than failing \u2014 that is a normal state, not an error.";
9285
+ var getAnalyticsSummaryContract = defineToolContract({
9286
+ name: "get_analytics_summary",
9287
+ agent: {
9288
+ description: `Read this project's Google Analytics 4 traffic \u2014 real numbers for questions like "did that launch land" or "which pages actually get read", instead of guessing. ${ANALYTICS_SHAPE}`,
9289
+ fields: {
9290
+ rangeDays: f.optional(f.number({ desc: ANALYTICS_RANGE, int: true, min: 1, max: 90 })),
9291
+ campaign: f.optional(f.string({ desc: ANALYTICS_CAMPAIGN, max: 200 }))
9292
+ }
9293
+ },
9294
+ mcp: {
9295
+ description: `Read a project's Google Analytics 4 traffic summary. ${ANALYTICS_SHAPE} Pass projectId to target a specific project; otherwise the configured default project is used.`,
9296
+ fields: {
9297
+ projectId: mcpProjectId,
9298
+ rangeDays: f.optional(f.number({ desc: ANALYTICS_RANGE, int: true, min: 1, max: 90 })),
9299
+ campaign: f.optional(f.string({ desc: ANALYTICS_CAMPAIGN, max: 200 }))
9300
+ }
9301
+ }
9302
+ });
9303
+ var integrationsContracts = [
9304
+ listProjectIntegrationsContract,
9305
+ listProjectChannelsContract,
9306
+ readChannelMessagesContract,
9307
+ postChannelMessageContract,
9308
+ getAnalyticsSummaryContract
9309
+ ];
9310
+ var MAX_CONTENT_CHARS = 1e6;
9311
+ var FILE_ID = "Drive file id, as returned by drive_list_files";
9312
+ var ROOT_DEFAULT = "Defaults to the project's connected root folder.";
9313
+ var MCP_PROJECT_TAIL = "Pass projectId to target a specific project; otherwise the configured default project is used.";
9314
+ var driveListFilesContract = defineToolContract({
9315
+ name: "drive_list_files",
9316
+ agent: {
9317
+ description: "List files and folders in the project's connected Google Drive folder. Omit folderId to list the project's root folder. Returns id, name, mimeType, size, and modified time for each entry.",
9318
+ fields: {
9319
+ folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
9320
+ search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
9321
+ limit: f.optional(f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 }))
9322
+ }
9323
+ },
9324
+ mcp: {
9325
+ description: `List files and folders in a project's connected Google Drive folder. Omit folderId to list the project's root folder. Returns id, name, mimeType, size, and modified time for each entry. ${MCP_PROJECT_TAIL}`,
9326
+ fields: {
9327
+ projectId: mcpProjectId,
9328
+ folderId: f.optional(f.string({ desc: `Folder to list. ${ROOT_DEFAULT}` })),
9329
+ search: f.optional(f.string({ desc: "Only return names containing this text", max: 200 })),
9330
+ limit: f.optional(f.number({ desc: "Max entries (default 100)", int: true, min: 1, max: 200 }))
9331
+ }
9332
+ }
9333
+ });
9334
+ var driveReadFileContract = defineToolContract({
9335
+ name: "drive_read_file",
9336
+ agent: {
9337
+ description: "Read a file's text content from the project's connected Google Drive folder. Google Docs, Sheets, and Slides are exported to text automatically. Content over 100 KB is truncated.",
9338
+ fields: { fileId: f.string({ desc: FILE_ID }) }
9339
+ },
9340
+ mcp: {
9341
+ description: `Read a file's text content from a project's connected Google Drive folder. Google Docs, Sheets, and Slides are exported to text automatically. Content over 100 KB is truncated. ${MCP_PROJECT_TAIL}`,
9342
+ fields: { projectId: mcpProjectId, fileId: f.string({ desc: FILE_ID }) }
9343
+ }
9344
+ });
9345
+ var driveCreateFileContract = defineToolContract({
9346
+ name: "drive_create_file",
9347
+ agent: {
9348
+ description: "Create a new file in the project's connected Google Drive folder. Use drive_update_file to change an existing file instead.",
9349
+ fields: {
9350
+ name: f.string({ desc: "File name, without any path separators", min: 1, max: 255 }),
9351
+ content: f.string({ desc: "File content, UTF-8 text", max: MAX_CONTENT_CHARS }),
9352
+ mimeType: f.optional(f.string({ desc: "MIME type (default text/plain)" })),
9353
+ folderId: f.optional(f.string({ desc: `Destination folder. ${ROOT_DEFAULT}` }))
9354
+ }
9355
+ },
9356
+ mcp: {
9357
+ description: `Create a new file in a project's connected Google Drive folder. Use drive_update_file to change an existing file instead. ${MCP_PROJECT_TAIL}`,
9358
+ fields: {
9359
+ projectId: mcpProjectId,
9360
+ name: f.string({ desc: "File name, without any path separators", min: 1, max: 255 }),
9361
+ content: f.string({ desc: "File content, UTF-8 text", max: MAX_CONTENT_CHARS }),
9362
+ mimeType: f.optional(f.string({ desc: "MIME type (default text/plain)" })),
9363
+ folderId: f.optional(f.string({ desc: `Destination folder. ${ROOT_DEFAULT}` }))
9364
+ }
9365
+ }
9366
+ });
9367
+ var driveUpdateFileContract = defineToolContract({
9368
+ name: "drive_update_file",
9369
+ agent: {
9370
+ description: "Replace the content of an existing file in the project's connected Google Drive folder. This overwrites the whole file. Google-native documents cannot be overwritten.",
9371
+ fields: {
9372
+ fileId: f.string({ desc: FILE_ID }),
9373
+ content: f.string({ desc: "Replacement content, UTF-8 text", max: MAX_CONTENT_CHARS }),
9374
+ mimeType: f.optional(f.string({ desc: "MIME type (defaults to the file's current type)" }))
9375
+ }
9376
+ },
9377
+ mcp: {
9378
+ description: `Replace the content of an existing file in a project's connected Google Drive folder. This overwrites the whole file \u2014 read it first if you are editing rather than replacing. Google-native documents cannot be overwritten. ${MCP_PROJECT_TAIL}`,
9379
+ fields: {
9380
+ projectId: mcpProjectId,
9381
+ fileId: f.string({ desc: FILE_ID }),
9382
+ content: f.string({ desc: "Replacement content, UTF-8 text", max: MAX_CONTENT_CHARS }),
9383
+ mimeType: f.optional(f.string({ desc: "MIME type (defaults to the file's current type)" }))
9384
+ }
9385
+ }
9386
+ });
9387
+ var driveDeleteFileContract = defineToolContract({
9388
+ name: "drive_delete_file",
9389
+ agent: {
9390
+ description: "Move a file in the project's connected Google Drive folder to the Drive trash. The file is recoverable from the trash; it is never permanently deleted.",
9391
+ fields: { fileId: f.string({ desc: FILE_ID }) }
9392
+ },
9393
+ mcp: {
9394
+ description: `Move a file in a project's connected Google Drive folder to the Drive trash. The file is recoverable from the trash; it is never permanently deleted. ${MCP_PROJECT_TAIL}`,
9395
+ fields: { projectId: mcpProjectId, fileId: f.string({ desc: FILE_ID }) }
9396
+ }
9397
+ });
9398
+ var driveCreateFolderContract = defineToolContract({
9399
+ name: "drive_create_folder",
9400
+ agent: {
9401
+ description: "Create a folder inside the project's connected Google Drive folder.",
9402
+ fields: {
9403
+ name: f.string({ desc: "Folder name, without any path separators", min: 1, max: 255 }),
9404
+ folderId: f.optional(f.string({ desc: `Parent folder. ${ROOT_DEFAULT}` }))
9405
+ }
9406
+ },
9407
+ mcp: {
9408
+ description: `Create a folder inside a project's connected Google Drive folder. ${MCP_PROJECT_TAIL}`,
9409
+ fields: {
9410
+ projectId: mcpProjectId,
9411
+ name: f.string({ desc: "Folder name, without any path separators", min: 1, max: 255 }),
9412
+ folderId: f.optional(f.string({ desc: `Parent folder. ${ROOT_DEFAULT}` }))
9413
+ }
9414
+ }
9415
+ });
9416
+ var driveContracts = [
9417
+ driveListFilesContract,
9418
+ driveReadFileContract,
9419
+ driveCreateFileContract,
9420
+ driveUpdateFileContract,
9421
+ driveDeleteFileContract,
9422
+ driveCreateFolderContract
9423
+ ];
9424
+ var MEETING_ID = "Meeting id, as returned by list_meetings.";
9425
+ var MCP_TAIL = "Pass projectId to target a specific project; otherwise the configured default project is used.";
9426
+ var SUMMARY_NOTE = 'A meeting whose summary is still being written reports status "processing" and a null summary \u2014 that is normal shortly after ingestion, not an error.';
9427
+ var listMeetingsContract = defineToolContract({
9428
+ name: "list_meetings",
9429
+ agent: {
9430
+ description: `List this project's meetings, newest first \u2014 each with its title, date, source, status, participants, and a short summary preview. Start here when you are asked about "the meeting", "what did we decide", or "what came out of that call". ${SUMMARY_NOTE}`,
9431
+ fields: {
9432
+ limit: f.optional(
9433
+ f.number({ desc: "How many to return (default 20, maximum 50).", int: true, min: 1, max: 50 })
9434
+ ),
9435
+ search: f.optional(
9436
+ f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
9437
+ )
9438
+ }
9439
+ },
9440
+ mcp: {
9441
+ description: `List a project's meetings, newest first, with title, date, source, status, participants, and a summary preview. ${SUMMARY_NOTE} ${MCP_TAIL}`,
9442
+ fields: {
9443
+ projectId: mcpProjectId,
9444
+ limit: f.optional(
9445
+ f.number({ desc: "How many to return (default 20, maximum 50).", int: true, min: 1, max: 50 })
9446
+ ),
9447
+ search: f.optional(
9448
+ f.string({ desc: "Case-insensitive match on the meeting title.", max: 200 })
9449
+ )
9450
+ }
9451
+ }
9452
+ });
9453
+ var getMeetingContract = defineToolContract({
9454
+ name: "get_meeting",
9455
+ agent: {
9456
+ description: `Read one meeting's details and its FULL AI summary \u2014 the overview, the decisions, and the proposed next steps. **Read this before reaching for the transcript**: the summary usually answers the question, and a transcript costs far more context. Also returns the participant list, the segment count, and the meeting's web link. ${SUMMARY_NOTE}`,
9457
+ fields: { meetingId: f.string({ desc: MEETING_ID }) }
9458
+ },
9459
+ mcp: {
9460
+ description: `Read one meeting's details and full AI summary (overview, decisions, proposed next steps), plus participants, segment count, and its web link. Prefer this over the transcript \u2014 the summary usually answers the question at a fraction of the context. ${SUMMARY_NOTE} ${MCP_TAIL}`,
9461
+ fields: { projectId: mcpProjectId, meetingId: f.string({ desc: MEETING_ID }) }
9462
+ }
9463
+ });
9464
+ var OFFSET_DESC = "Segment index to start from (default 0). Pass the nextOffset from the previous page to continue.";
9465
+ var LIMIT_DESC2 = "Segments per page (default 200, maximum 500). A long meeting runs to thousands, so page it rather than asking for everything.";
9466
+ var readMeetingTranscriptContract = defineToolContract({
9467
+ name: "read_meeting_transcript",
9468
+ agent: {
9469
+ description: `Read a page of a meeting's raw transcript as \`[hh:mm:ss] Speaker: text\` lines. Use this when you need someone's exact words \u2014 a quote, a caveat, or the reasoning behind a decision the summary only states. For "what happened in the meeting", read get_meeting instead. The response carries nextOffset when more segments remain.`,
9470
+ fields: {
9471
+ meetingId: f.string({ desc: MEETING_ID }),
9472
+ offset: f.optional(f.number({ desc: OFFSET_DESC, int: true, min: 0 })),
9473
+ limit: f.optional(f.number({ desc: LIMIT_DESC2, int: true, min: 1, max: 500 }))
9474
+ }
9475
+ },
9476
+ mcp: {
9477
+ description: `Read a page of a meeting's raw transcript as \`[hh:mm:ss] Speaker: text\` lines. Use it for exact wording; prefer get_meeting's summary for what happened. The response carries nextOffset when more segments remain. ${MCP_TAIL}`,
9478
+ fields: {
9479
+ projectId: mcpProjectId,
9480
+ meetingId: f.string({ desc: MEETING_ID }),
9481
+ offset: f.optional(f.number({ desc: OFFSET_DESC, int: true, min: 0 })),
9482
+ limit: f.optional(f.number({ desc: LIMIT_DESC2, int: true, min: 1, max: 500 }))
9483
+ }
9484
+ }
9485
+ });
9486
+ var meetingsContracts = [
9487
+ listMeetingsContract,
9488
+ getMeetingContract,
9489
+ readMeetingTranscriptContract
9490
+ ];
9491
+ var SINCE_MINUTES = f.optional(
9492
+ f.number({
9493
+ desc: "Relative time window ending now, in minutes (default 60). Ignored if startTime is set.",
9494
+ int: true,
9495
+ min: 1,
9496
+ max: 10080
9497
+ })
9498
+ );
9499
+ var START_TIME = f.optional(
9500
+ f.string({ desc: "ISO 8601 lower bound (overrides sinceMinutes)" })
9501
+ );
9502
+ var END_TIME = f.optional(f.string({ desc: "ISO 8601 upper bound (default now)" }));
9503
+ var LIMIT = f.optional(
9504
+ f.number({ desc: "Max entries per page (default 50)", int: true, min: 1, max: 200 })
9505
+ );
9506
+ var RAW_QUERY_MAX = 2e3;
9507
+ var gcpFields = {
9508
+ env: f.optional(
9509
+ f.enum(["prod", "dev", "claudespace"], {
9510
+ desc: "GCP environment slot to query (default prod)"
9511
+ })
9512
+ ),
9513
+ sinceMinutes: SINCE_MINUTES,
9514
+ startTime: START_TIME,
9515
+ endTime: END_TIME,
9516
+ severity: f.optional(
9517
+ f.enum(SEVERITY_ENUM, {
9518
+ desc: "Minimum severity, inclusive \u2014 ERROR returns ERROR and above"
9519
+ })
9520
+ ),
9521
+ services: f.optional(
9522
+ f.array(f.string(), {
9523
+ desc: "Restrict to these Cloud Run service names (prod/dev only). Defaults to all services linked in project settings."
9524
+ })
9525
+ ),
9526
+ sqlInstances: f.optional(
9527
+ f.array(f.string(), { desc: "Restrict to these Cloud SQL instance names (prod/dev only)" })
9528
+ ),
9529
+ allServices: f.optional(
9530
+ f.boolean({
9531
+ desc: "Set true to search ALL logs in the GCP project, ignoring the linked-resource scope"
9532
+ })
9533
+ ),
9534
+ search: f.optional(
9535
+ f.string({ desc: "Free-text search across all log fields (exact substring, not regex)", max: 256 })
9536
+ ),
9537
+ filter: f.optional(
9538
+ f.string({
9539
+ desc: "Advanced: raw Cloud Logging filter expression, ANDed with the scope (it cannot widen it)",
9540
+ max: RAW_QUERY_MAX
9541
+ })
9542
+ ),
9543
+ limit: LIMIT,
9544
+ pageToken: f.optional(
9545
+ f.string({ desc: "Opaque token from a previous response to fetch the next page" })
9546
+ )
9547
+ };
9548
+ 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.";
9549
+ var queryGcpLogsContract = defineToolContract({
9550
+ name: "query_gcp_logs",
9551
+ agent: {
9552
+ description: `Query Google Cloud Logging for this project's linked GCP environments \u2014 use this to investigate a production or dev failure directly rather than guessing from the code. ${GCP_SHARED_DESC}`,
9553
+ fields: gcpFields
9554
+ },
9555
+ mcp: {
9556
+ description: `Query Google Cloud Logging for a project's linked GCP environments \u2014 use this to investigate production or dev issues directly ('something broke on prod'). ${GCP_SHARED_DESC} Pass projectId to target a specific project; otherwise the configured default project is used.`,
9557
+ fields: { projectId: mcpProjectId, ...gcpFields }
9121
9558
  }
9122
9559
  });
9123
- var pullRequestContracts = [createPullRequestContract];
9560
+ var grafanaFields = {
9561
+ env: f.optional(
9562
+ f.enum(["prod", "dev"], {
9563
+ desc: "Configured Grafana env mapping to scope by (default prod)"
9564
+ })
9565
+ ),
9566
+ sinceMinutes: SINCE_MINUTES,
9567
+ startTime: START_TIME,
9568
+ endTime: END_TIME,
9569
+ level: f.optional(
9570
+ f.enum(["debug", "info", "warn", "error", "fatal"], {
9571
+ desc: "Minimum severity, inclusive \u2014 error returns error and above"
9572
+ })
9573
+ ),
9574
+ services: f.optional(
9575
+ f.array(f.string(), { desc: "Restrict to these service_name label values" })
9576
+ ),
9577
+ search: f.optional(
9578
+ f.string({ desc: "Substring line filter (exact substring, not regex)", max: 256 })
9579
+ ),
9580
+ logql: f.optional(
9581
+ f.string({
9582
+ desc: "Advanced: raw LogQL query \u2014 REPLACES env/services/level/search composition",
9583
+ max: RAW_QUERY_MAX
9584
+ })
9585
+ ),
9586
+ limit: LIMIT
9587
+ };
9588
+ var GRAFANA_SHARED_DESC = "the APPLICATION logs shipped to Grafana Cloud/Loki, complementing query_gcp_logs (GCP infrastructure logs). Start with structured filters (env, level=error, sinceMinutes=60, services), then narrow with search; pass raw LogQL via logql only when structured filters cannot express the query (it REPLACES them). The response header echoes the composed LogQL \u2014 iterate on it. Returns compact lines: '<time> <SEVERITY> [<service>] <message>'.";
9589
+ var queryGrafanaLogsContract = defineToolContract({
9590
+ name: "query_grafana_logs",
9591
+ agent: {
9592
+ description: `Query this project's connected Grafana (Loki) logs \u2014 ${GRAFANA_SHARED_DESC}`,
9593
+ fields: grafanaFields
9594
+ },
9595
+ mcp: {
9596
+ description: `Query the project's connected Grafana (Loki) logs \u2014 ${GRAFANA_SHARED_DESC} Pass projectId to target a specific project; otherwise the configured default project is used.`,
9597
+ fields: { projectId: mcpProjectId, ...grafanaFields }
9598
+ }
9599
+ });
9600
+ var logsContracts = [
9601
+ queryGcpLogsContract,
9602
+ queryGrafanaLogsContract
9603
+ ];
9124
9604
  var TOOL_CONTRACTS = Object.fromEntries(
9125
9605
  [
9126
9606
  ...tasksContracts,
9607
+ ...taskUpdateContracts,
9127
9608
  ...tagsContracts,
9128
9609
  ...checklistContracts,
9129
9610
  ...dependenciesContracts,
9130
9611
  ...subtasksContracts,
9131
9612
  ...attachmentsContracts,
9132
9613
  ...suggestionsContracts,
9133
- ...pullRequestContracts
9614
+ ...pullRequestContracts,
9615
+ ...integrationsContracts,
9616
+ ...driveContracts,
9617
+ ...meetingsContracts,
9618
+ ...logsContracts
9134
9619
  ].map((contract) => [contract.name, contract])
9135
9620
  );
9136
9621
 
9137
9622
  // src/tools/contract-tool.ts
9138
- import { z as z10 } from "zod";
9623
+ import { z as z12 } from "zod";
9139
9624
  function agentShape(surface) {
9140
- return compileShape(z10, surface.fields);
9625
+ return compileShape(z12, surface.fields);
9141
9626
  }
9142
9627
  function defineContractTool(contract, handler, options) {
9143
9628
  return {
@@ -9222,6 +9707,54 @@ ${plan}` : plan
9222
9707
  { annotations: { readOnlyHint: true } }
9223
9708
  );
9224
9709
  }
9710
+ function buildGetConnectionContextTool(connection) {
9711
+ return defineContractTool(
9712
+ getConnectionContextContract,
9713
+ async () => {
9714
+ try {
9715
+ const ctx = await connection.call("getTaskContext", {
9716
+ sessionId: connection.sessionId,
9717
+ peekPlanRevision: true
9718
+ });
9719
+ return textResult(
9720
+ JSON.stringify(
9721
+ {
9722
+ task: {
9723
+ id: ctx.id,
9724
+ title: ctx.title,
9725
+ status: ctx.status,
9726
+ branch: ctx.githubBranch,
9727
+ baseBranch: ctx.baseBranch,
9728
+ isParentTask: ctx.isParentTask ?? false,
9729
+ parentTaskId: ctx.parentTaskId,
9730
+ // Safe to report BECAUSE we peeked: the marker is still there
9731
+ // for get_current_plan to consume and banner properly.
9732
+ planRevisedAt: ctx.planRevisedAt ?? null
9733
+ },
9734
+ project: { id: ctx.projectId, name: ctx.projectName ?? null },
9735
+ agent: {
9736
+ id: ctx.agentId,
9737
+ model: ctx.model,
9738
+ mode: ctx.agentMode ?? null,
9739
+ isAuto: ctx.isAuto ?? false
9740
+ },
9741
+ // Said plainly so a shared skill does not go looking for an
9742
+ // account here: a pod is bound to a card, not to a person.
9743
+ summary: `Pod session bound to task ${ctx.id} on branch ${ctx.githubBranch ?? "(none yet)"} (base ${ctx.baseBranch ?? "(none)"}) in project ${ctx.projectName ?? ctx.projectId}. No user account is attached to a pod session.`
9744
+ },
9745
+ null,
9746
+ 2
9747
+ )
9748
+ );
9749
+ } catch (error) {
9750
+ return textResult(
9751
+ `Failed to resolve connection context: ${error instanceof Error ? error.message : "Unknown error"}`
9752
+ );
9753
+ }
9754
+ },
9755
+ { annotations: { readOnlyHint: true } }
9756
+ );
9757
+ }
9225
9758
  function buildGetTaskTool(connection) {
9226
9759
  return defineContractTool(
9227
9760
  getTaskContract,
@@ -9246,11 +9779,11 @@ function buildGetExecutionLogsTool(connection) {
9246
9779
  "get_execution_logs",
9247
9780
  "Read CLI execution logs \u2014 agent reasoning, tool calls, and setup/dev-server output. Filter via source='agent' or 'application'. For human chat use read_task_chat.",
9248
9781
  {
9249
- task_id: z11.string().optional().describe(
9782
+ task_id: z13.string().optional().describe(
9250
9783
  "Task ID or slug. Omit to read logs from the current task. Only the current task or one of its child tasks can be read."
9251
9784
  ),
9252
- source: z11.enum(["agent", "application"]).optional().describe("Filter by log source. Omit for all logs."),
9253
- limit: z11.number().optional().describe("Max number of log entries to return (default 50, max 500).")
9785
+ source: z13.enum(["agent", "application"]).optional().describe("Filter by log source. Omit for all logs."),
9786
+ limit: z13.number().optional().describe("Max number of log entries to return (default 50, max 500).")
9254
9787
  },
9255
9788
  async ({ task_id, source, limit }) => {
9256
9789
  try {
@@ -9336,6 +9869,7 @@ function buildTaskContextTools(connection) {
9336
9869
  return [
9337
9870
  buildReadTaskChatTool(connection),
9338
9871
  buildGetCurrentPlanTool(connection),
9872
+ buildGetConnectionContextTool(connection),
9339
9873
  buildGetTaskTool(connection),
9340
9874
  buildGetExecutionLogsTool(connection),
9341
9875
  buildListTaskFilesTool(connection),
@@ -9344,7 +9878,7 @@ function buildTaskContextTools(connection) {
9344
9878
  }
9345
9879
 
9346
9880
  // src/tools/dependency-suggestion-tools.ts
9347
- import { z as z12 } from "zod";
9881
+ import { z as z14 } from "zod";
9348
9882
  function buildGetDependenciesTool(connection) {
9349
9883
  return defineContractTool(
9350
9884
  getDependenciesContract,
@@ -9368,10 +9902,10 @@ function buildGetSuggestionsTool(connection) {
9368
9902
  "get_suggestions",
9369
9903
  "List project suggestions sorted by vote score. Filter by status or cap with limit (default 20). Suggestions are project-level ideas, not tasks \u2014 use get_task for tasks.",
9370
9904
  {
9371
- status: z12.string().optional().describe(
9905
+ status: z14.string().optional().describe(
9372
9906
  "Filter by status: Planning, Open, InProgress, ReviewPR, ReviewDev, ReviewLive, Complete, Cancelled"
9373
9907
  ),
9374
- limit: z12.number().int().min(1).max(100).optional().describe("Max results (default 20)")
9908
+ limit: z14.number().int().min(1).max(100).optional().describe("Max results (default 20)")
9375
9909
  },
9376
9910
  async ({ status, limit }) => {
9377
9911
  try {
@@ -9395,7 +9929,7 @@ function buildGetSuggestionsTool(connection) {
9395
9929
  }
9396
9930
 
9397
9931
  // src/tools/mutation-tools.ts
9398
- import { z as z13 } from "zod";
9932
+ import { z as z15 } from "zod";
9399
9933
 
9400
9934
  // src/runner/refresh-verify-heal.ts
9401
9935
  async function refreshAndVerifyGithubCredential(cwd, mint) {
@@ -9527,7 +10061,7 @@ function buildForceUpdateTaskStatusTool(connection) {
9527
10061
  "force_update_task_status",
9528
10062
  "EMERGENCY ONLY: force-override a task's Kanban status. Use when an automatic transition failed and the task is wedged. Normal flow transitions status automatically.",
9529
10063
  {
9530
- status: z13.enum([
10064
+ status: z15.enum([
9531
10065
  "Planning",
9532
10066
  "Open",
9533
10067
  "InProgress",
@@ -9537,7 +10071,7 @@ function buildForceUpdateTaskStatusTool(connection) {
9537
10071
  "Complete",
9538
10072
  "Cancelled"
9539
10073
  ]).describe("The new status for the task"),
9540
- task_id: z13.string().optional().describe("Child task ID to update. Omit to update the current task.")
10074
+ task_id: z15.string().optional().describe("Child task ID to update. Omit to update the current task.")
9541
10075
  },
9542
10076
  async ({ status, task_id }) => {
9543
10077
  try {
@@ -9690,10 +10224,10 @@ function buildCreateFollowUpTaskTool(connection) {
9690
10224
  "create_follow_up_task",
9691
10225
  "Create a follow-up task that depends on the current task. The new card is a SIBLING of this one (same parent) and is blocked until this task merges \u2014 it is NOT a child of this card. To break this card into child cards that build as a pack, use create_subtask. For blockers use add_dependency.",
9692
10226
  {
9693
- title: z13.string().describe("Follow-up task title"),
9694
- description: z13.string().optional().describe(cardDescriptionDesc("Brief description of the follow-up work")),
9695
- plan: z13.string().optional().describe("Implementation plan if known"),
9696
- story_point_value: z13.number().optional().describe("Story point estimate (1=Common, 2=Magic, 3=Rare, 5=Unique)")
10227
+ title: z15.string().describe("Follow-up task title"),
10228
+ description: z15.string().optional().describe(cardDescriptionDesc("Brief description of the follow-up work")),
10229
+ plan: z15.string().optional().describe("Implementation plan if known"),
10230
+ story_point_value: z15.number().optional().describe("Story point estimate (1=Common, 2=Magic, 3=Rare, 5=Unique)")
9697
10231
  },
9698
10232
  async ({ title, description, plan, story_point_value }) => {
9699
10233
  try {
@@ -9720,11 +10254,11 @@ function buildCreateSuggestionTool(connection) {
9720
10254
  "create_suggestion",
9721
10255
  "Suggest a feature, improvement, rule, or idea for the project. Duplicates are deduped and your upvote is recorded. For actionable work on this task open a follow-up task.",
9722
10256
  {
9723
- title: z13.string().describe("Short title for the suggestion"),
9724
- description: z13.string().optional().describe(
10257
+ title: z15.string().describe("Short title for the suggestion"),
10258
+ description: z15.string().optional().describe(
9725
10259
  "1-2 sentence description of what should change and why. Keep concise and project-focused."
9726
10260
  ),
9727
- tag_names: z13.array(z13.string()).optional().describe("Tag names to categorize the suggestion")
10261
+ tag_names: z15.array(z15.string()).optional().describe("Tag names to categorize the suggestion")
9728
10262
  },
9729
10263
  async ({ title, description, tag_names }) => {
9730
10264
  try {
@@ -9753,8 +10287,8 @@ function buildVoteSuggestionTool(connection) {
9753
10287
  "vote_suggestion",
9754
10288
  "Vote +1 or -1 on a project suggestion. Use to express support or disagreement with a specific suggestion returned by get_suggestions.",
9755
10289
  {
9756
- suggestion_id: z13.string().describe("The suggestion ID to vote on"),
9757
- value: z13.number().refine((v) => v === 1 || v === -1, { message: "Value must be 1 or -1" }).describe("+1 to upvote, -1 to downvote")
10290
+ suggestion_id: z15.string().describe("The suggestion ID to vote on"),
10291
+ value: z15.number().refine((v) => v === 1 || v === -1, { message: "Value must be 1 or -1" }).describe("+1 to upvote, -1 to downvote")
9758
10292
  },
9759
10293
  async ({ suggestion_id, value }) => {
9760
10294
  try {
@@ -10135,14 +10669,81 @@ function buildCommonTools(connection, config) {
10135
10669
  }
10136
10670
 
10137
10671
  // src/tools/pm-tools.ts
10138
- import { z as z14 } from "zod";
10672
+ import { z as z16 } from "zod";
10673
+
10674
+ // src/tools/task-update-tools.ts
10675
+ var CURRENT_TASK_ONLY = ["title", "description", "plan", "githubBranch"];
10676
+ function presentCurrentTaskFields(input) {
10677
+ return CURRENT_TASK_ONLY.filter((key) => input[key] !== void 0);
10678
+ }
10679
+ function rejectionFor(input, present) {
10680
+ if (input.task_id && present.length > 0) {
10681
+ return `Cannot update ${present.join(", ")} on another task: task_id is only valid with status. Call update_task again without task_id to change the current task, and use a separate call with task_id to move the child.`;
10682
+ }
10683
+ if (present.length === 0 && input.status === void 0) {
10684
+ return "No fields to update. Pass at least one of: title, description, plan, status, githubBranch.";
10685
+ }
10686
+ return null;
10687
+ }
10688
+ async function applyToCurrentTask(connection, input, applied) {
10689
+ if (input.plan !== void 0 || input.description !== void 0) {
10690
+ await connection.call("updateTaskFields", {
10691
+ sessionId: connection.sessionId,
10692
+ plan: input.plan,
10693
+ description: input.description
10694
+ });
10695
+ if (input.plan !== void 0) applied.push("plan");
10696
+ if (input.description !== void 0) applied.push("description");
10697
+ }
10698
+ if (input.title !== void 0 || input.githubBranch !== void 0) {
10699
+ await connection.call("updateTaskProperties", {
10700
+ sessionId: connection.sessionId,
10701
+ title: input.title,
10702
+ githubBranch: input.githubBranch
10703
+ });
10704
+ if (input.title !== void 0) applied.push("title");
10705
+ if (input.githubBranch !== void 0) applied.push("githubBranch");
10706
+ }
10707
+ if (input.status !== void 0) {
10708
+ await connection.call("updateTaskStatus", {
10709
+ sessionId: connection.sessionId,
10710
+ status: input.status
10711
+ });
10712
+ applied.push(`status: ${input.status}`);
10713
+ }
10714
+ }
10715
+ function buildTaskUpdateTool(connection) {
10716
+ return defineContractTool(updateTaskContract, async (input) => {
10717
+ const rejection = rejectionFor(input, presentCurrentTaskFields(input));
10718
+ if (rejection) return textResult(rejection);
10719
+ const applied = [];
10720
+ try {
10721
+ if (input.task_id) {
10722
+ await connection.call("updateChildStatus", {
10723
+ sessionId: connection.sessionId,
10724
+ childTaskId: input.task_id,
10725
+ status: input.status
10726
+ });
10727
+ return textResult(`Child task ${input.task_id} status updated to ${input.status}.`);
10728
+ }
10729
+ await applyToCurrentTask(connection, input, applied);
10730
+ return textResult(`Task updated \xB7 ${applied.join(", ")}`);
10731
+ } catch (error) {
10732
+ const detail = error instanceof Error ? error.message : String(error);
10733
+ const partial = applied.length > 0 ? ` Already applied: ${applied.join(", ")}.` : "";
10734
+ return textResult(`Failed to update task: ${detail}.${partial}`);
10735
+ }
10736
+ });
10737
+ }
10738
+
10739
+ // src/tools/pm-tools.ts
10139
10740
  function buildUpdateTaskTool(connection) {
10140
10741
  return defineTool(
10141
10742
  "update_task_plan",
10142
10743
  "Save the plan and/or description to the current task. In auto/building mode, save the plan BEFORE writing code and keep it current as the approach evolves \u2014 post it, then build; never pause the build waiting for approval. For children use update_subtask; for title/tags/PR use update_task_properties.",
10143
10744
  {
10144
- plan: z14.string().optional().describe("The task plan in markdown"),
10145
- description: z14.string().optional().describe(cardDescriptionDesc("Updated task description"))
10745
+ plan: z16.string().optional().describe("The task plan in markdown"),
10746
+ description: z16.string().optional().describe(cardDescriptionDesc("Updated task description"))
10146
10747
  },
10147
10748
  async ({ plan, description }) => {
10148
10749
  try {
@@ -10163,12 +10764,12 @@ function buildUpdateTaskTool(connection) {
10163
10764
  function buildHandoffTool(connection) {
10164
10765
  return defineTool(
10165
10766
  "handoff_to_agent",
10166
- "Hand this task off to an implementer agent for the build phase \u2014 mid-conversation, same session, no restart. Call this once the plan is compiled and saved (update_task_plan). The server swaps this task to the difficulty-sized implementer agent (which may run at a different model level), announces the handoff in the activity log + chat, and switches you into build mode to start implementing. Size the work with the storyPoints arg (or set it first via update_task_properties). Returns the implementer's name + model.",
10767
+ "Hand this task off to an implementer agent for the build phase \u2014 mid-conversation, same session, no restart. Call this once the plan is compiled and saved (update_task). The server swaps this task to the difficulty-sized implementer agent (which may run at a different model level), announces the handoff in the activity log + chat, and switches you into build mode to start implementing. Size the work with the storyPoints arg (or set it first via update_task_properties). Returns the implementer's name + model.",
10167
10768
  {
10168
- storyPoints: z14.number().int().positive().optional().describe(
10769
+ storyPoints: z16.number().int().positive().optional().describe(
10169
10770
  "Difficulty sizing (1=Common, 2=Magic, 3=Rare, 5=Unique, 8=Pack) \u2014 picks which implementer agent takes over. Omit to use the task's current story points."
10170
10771
  ),
10171
- message: z14.string().optional().describe("Optional kickoff note posted to the chat alongside the handoff notice.")
10772
+ message: z16.string().optional().describe("Optional kickoff note posted to the chat alongside the handoff notice.")
10172
10773
  },
10173
10774
  async ({ storyPoints, message }) => {
10174
10775
  try {
@@ -10319,7 +10920,7 @@ function buildPackTools(connection) {
10319
10920
  "start_child_cloud_build",
10320
10921
  "Start a cloud build (codespace) for a child task. Preconditions: child status is `Open`, story points set, and an agent assigned \u2014 satisfy all three with update_subtask (status/agentIdOrName/storyPointValue) first; none happen automatically. A PACK_CHILD_LIMIT error is backpressure, not failure: check list_subtasks packSlots for which children hold the in-flight slots, merge/stop one, then retry. On a feature-branch pack, dev is merged into the pack branch first so the child branches from a fresh base; a conflict is reported in the result and never blocks the launch.",
10321
10922
  {
10322
- childTaskId: z14.string().describe("The child task ID to start a cloud build for")
10923
+ childTaskId: z16.string().describe("The child task ID to start a cloud build for")
10323
10924
  },
10324
10925
  async ({ childTaskId }) => {
10325
10926
  try {
@@ -10348,7 +10949,7 @@ Base sync: dev \u2192 ${sync.branch} failed \u2014 ${sync.error}. The child was
10348
10949
  "stop_child_build",
10349
10950
  "Send a graceful stop signal to a running child build's agent. Not a force-kill \u2014 the agent may take a moment to wind down. Stopping a child eventually frees its PACK_CHILD_LIMIT build slot (see list_subtasks packSlots).",
10350
10951
  {
10351
- childTaskId: z14.string().describe("The child task ID whose build should be stopped")
10952
+ childTaskId: z16.string().describe("The child task ID whose build should be stopped")
10352
10953
  },
10353
10954
  async ({ childTaskId }) => {
10354
10955
  try {
@@ -10389,6 +10990,13 @@ Base sync: dev \u2192 ${sync.branch} failed \u2014 ${sync.error}. The child was
10389
10990
  function buildPmTools(connection, options) {
10390
10991
  const tools = [
10391
10992
  buildUpdateTaskTool(connection),
10993
+ // The shared-skill vocabulary alongside the split legacy tools above.
10994
+ // Both stay REGISTERED, but the migration this comment used to defer has
10995
+ // now happened (A2-5/A2-6): no prompt names `update_task_plan` any more, so
10996
+ // it left ALWAYS_LOADED_TOOLS and is reachable via ToolSearch instead.
10997
+ // `update_task_properties` deliberately stayed hot — it is the only pod tool
10998
+ // carrying storyPointValue and risk, which ExitPlanMode requires.
10999
+ buildTaskUpdateTool(connection),
10392
11000
  buildCreateSubtaskTool(connection),
10393
11001
  buildSetTaskParentTool(connection),
10394
11002
  buildUpdateSubtaskTool(connection),
@@ -10400,7 +11008,7 @@ function buildPmTools(connection, options) {
10400
11008
  }
10401
11009
 
10402
11010
  // src/tools/discovery-tools.ts
10403
- import { z as z15 } from "zod";
11011
+ import { z as z17 } from "zod";
10404
11012
  var SP_DESCRIPTION2 = "Story point value (1=Common, 2=Magic, 3=Rare, 5=Unique). The key is 'storyPointValue' \u2014 not 'storyPoints'.";
10405
11013
  var VALID_PROPERTY_KEYS = "title, storyPointValue, tagNames, githubPRUrl, githubBranch, risk";
10406
11014
  function describeUpdatedFields(p) {
@@ -10419,12 +11027,12 @@ function buildDiscoveryTools(connection) {
10419
11027
  "update_task_properties",
10420
11028
  "Set one or more task properties in a single call. Valid keys: title, storyPointValue, tagNames, githubPRUrl, githubBranch, risk. All are optional \u2014 include only the ones you want to update (at least one). Unknown keys are rejected.",
10421
11029
  {
10422
- title: z15.string().optional().describe("The new task title"),
10423
- storyPointValue: z15.number().optional().describe(SP_DESCRIPTION2),
10424
- tagNames: z15.array(z15.string()).optional().describe("Array of tag names to assign"),
10425
- githubPRUrl: z15.string().url().optional().describe("GitHub pull request URL to link to this task"),
10426
- githubBranch: z15.string().optional().describe("Set the GitHub branch name for this task (e.g. 'conveyor/my-feature-abc123')"),
10427
- risk: z15.enum(["critical", "high", "medium", "low"]).nullable().optional().describe(
11030
+ title: z17.string().optional().describe("The new task title"),
11031
+ storyPointValue: z17.number().optional().describe(SP_DESCRIPTION2),
11032
+ tagNames: z17.array(z17.string()).optional().describe("Array of tag names to assign"),
11033
+ githubPRUrl: z17.string().url().optional().describe("GitHub pull request URL to link to this task"),
11034
+ githubBranch: z17.string().optional().describe("Set the GitHub branch name for this task (e.g. 'conveyor/my-feature-abc123')"),
11035
+ risk: z17.enum(["critical", "high", "medium", "low"]).nullable().optional().describe(
10428
11036
  "Risk level \u2014 how much important surface the task touches (critical/high/medium/low). Pass null to clear."
10429
11037
  )
10430
11038
  },
@@ -10464,7 +11072,7 @@ function buildDiscoveryTools(connection) {
10464
11072
  }
10465
11073
 
10466
11074
  // src/tools/project-tools.ts
10467
- import { z as z16 } from "zod";
11075
+ import { z as z18 } from "zod";
10468
11076
 
10469
11077
  // src/execution/context-path-verifier.ts
10470
11078
  import { readFile as readFile2 } from "fs/promises";
@@ -10542,16 +11150,16 @@ function formatContextPathProblems(problems) {
10542
11150
  }
10543
11151
 
10544
11152
  // src/tools/project-tools.ts
10545
- var CONTEXT_PATH_SHAPE = z16.object({
10546
- type: z16.enum(["rule", "doc", "file", "folder"]).describe(
11153
+ var CONTEXT_PATH_SHAPE = z18.object({
11154
+ type: z18.enum(["rule", "doc", "file", "folder"]).describe(
10547
11155
  "Link kind \u2014 all paths are repo-relative; doc marks a synced project doc, which resolves from the workspace like rule/file"
10548
11156
  ),
10549
- path: z16.string().min(1).max(500).describe("Repo-relative path"),
10550
- label: z16.string().max(100).optional(),
10551
- locator: z16.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional().describe(
11157
+ path: z18.string().min(1).max(500).describe("Repo-relative path"),
11158
+ label: z18.string().max(100).optional(),
11159
+ locator: z18.string().min(1).max(CONTEXT_LINK_LOCATOR_MAX).regex(/^[^\r\n]*$/, "Locator cannot contain line breaks").optional().describe(
10552
11160
  'Verified-link tether: text that must keep existing in the file. With locatorType "test" it must appear inside a real it/test/describe TITLE; with "code" anywhere in the file. Validated at write time against the checkout and re-checked by the periodic sweep \u2014 a rename/delete flags the link stale. Locators containing <> are placeholders and never checked.'
10553
11161
  ),
10554
- locatorType: z16.enum(["test", "code"]).optional().describe("How the locator must match \u2014 required iff locator is set; not valid on folder links")
11162
+ locatorType: z18.enum(["test", "code"]).optional().describe("How the locator must match \u2014 required iff locator is set; not valid on folder links")
10555
11163
  }).refine((link) => link.locator === void 0 === (link.locatorType === void 0), {
10556
11164
  message: "locator and locatorType must be provided together"
10557
11165
  }).refine((link) => link.locator === void 0 || link.type !== "folder", {
@@ -10598,15 +11206,15 @@ function buildCreateTagTool(connection, projectId, workspaceDir) {
10598
11206
  "create_tag",
10599
11207
  "Create a project tag. Include a crisp description (\u2264255 chars \u2014 the summary) and contextPaths (rule/doc/file/folder links agents auto-load when working on matching tasks); put the full spec in overview. Set parentTagIds to place the tag in the hierarchy right away. Every contextPath is checked against the repo checkout \u2014 a path that does not exist, or whose type does not match what is on disk, rejects the whole call. Fails if the name already exists.",
10600
11208
  {
10601
- name: z16.string().min(1).max(50),
10602
- color: z16.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("#RRGGBB (default gray)"),
10603
- description: z16.string().max(TAG_DESCRIPTION_MAX).optional(),
10604
- overview: z16.string().max(TAG_OVERVIEW_MAX).optional().describe("Full markdown glossary body \u2014 the term's spec"),
10605
- overviewPath: z16.string().min(1).max(500).optional().describe(
11209
+ name: z18.string().min(1).max(50),
11210
+ color: z18.string().regex(/^#[0-9a-fA-F]{6}$/).optional().describe("#RRGGBB (default gray)"),
11211
+ description: z18.string().max(TAG_DESCRIPTION_MAX).optional(),
11212
+ overview: z18.string().max(TAG_OVERVIEW_MAX).optional().describe("Full markdown glossary body \u2014 the term's spec"),
11213
+ overviewPath: z18.string().min(1).max(500).optional().describe(
10606
11214
  "Repo file to source the overview from (base-branch content is served everywhere). A not-yet-merged path is fine \u2014 the tag serves the stored overview as fallback until the file lands."
10607
11215
  ),
10608
- parentTagIds: z16.array(z16.string()).max(25).optional().describe("Parent tag ids from list_tags (multi-parent DAG) to link at create time"),
10609
- contextPaths: z16.array(CONTEXT_PATH_SHAPE).max(20).optional()
11216
+ parentTagIds: z18.array(z18.string()).max(25).optional().describe("Parent tag ids from list_tags (multi-parent DAG) to link at create time"),
11217
+ contextPaths: z18.array(CONTEXT_PATH_SHAPE).max(20).optional()
10610
11218
  },
10611
11219
  async ({ name, color, description, overview, overviewPath, parentTagIds, contextPaths }) => {
10612
11220
  const rejection = await rejectBadContextPaths(contextPaths, workspaceDir);
@@ -10634,19 +11242,19 @@ function buildUpdateTagTool(connection, projectId, taskId, workspaceDir) {
10634
11242
  "update_tag",
10635
11243
  "Update a tag's name, color, description (\u2264255), markdown overview, parent tags, or contextPaths. contextPaths and parentTagIds are FULL replacements \u2014 include what you want to keep. ALWAYS pass a short reason; it lands in the tag's revision history (with your current card auto-stamped) so the team sees why the glossary changed. Every contextPath is checked against the repo checkout \u2014 a path that does not exist, or whose type does not match what is on disk, rejects the whole call and nothing is written.",
10636
11244
  {
10637
- tagId: z16.string().describe("Tag id from list_tags"),
10638
- name: z16.string().min(1).max(50).optional(),
10639
- color: z16.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
10640
- description: z16.string().max(TAG_DESCRIPTION_MAX).optional(),
10641
- overview: z16.string().max(TAG_OVERVIEW_MAX).nullable().optional().describe(
11245
+ tagId: z18.string().describe("Tag id from list_tags"),
11246
+ name: z18.string().min(1).max(50).optional(),
11247
+ color: z18.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
11248
+ description: z18.string().max(TAG_DESCRIPTION_MAX).optional(),
11249
+ overview: z18.string().max(TAG_OVERVIEW_MAX).nullable().optional().describe(
10642
11250
  "Full markdown glossary body; null clears it. REJECTED while overviewPath is set \u2014 edit the sourced file in the repo instead"
10643
11251
  ),
10644
- overviewPath: z16.string().min(1).max(500).nullable().optional().describe(
11252
+ overviewPath: z18.string().min(1).max(500).nullable().optional().describe(
10645
11253
  "Repo file to source the overview from (null clears back to the stored overview). A not-yet-merged path is fine \u2014 the stored overview serves as fallback until the file lands on the base branch."
10646
11254
  ),
10647
- parentTagIds: z16.array(z16.string()).max(25).optional().describe("Full-set replacement of the tag's parent tags (multi-parent DAG)"),
10648
- reason: z16.string().max(TAG_REASON_MAX).optional().describe("One line on why \u2014 shown in the tag's revision history"),
10649
- contextPaths: z16.array(CONTEXT_PATH_SHAPE).max(20).optional()
11255
+ parentTagIds: z18.array(z18.string()).max(25).optional().describe("Full-set replacement of the tag's parent tags (multi-parent DAG)"),
11256
+ reason: z18.string().max(TAG_REASON_MAX).optional().describe("One line on why \u2014 shown in the tag's revision history"),
11257
+ contextPaths: z18.array(CONTEXT_PATH_SHAPE).max(20).optional()
10650
11258
  },
10651
11259
  async ({
10652
11260
  tagId,
@@ -10704,8 +11312,8 @@ function buildPostToProjectChatTool(connection, projectId) {
10704
11312
  "post_to_project_chat",
10705
11313
  "Post a markdown message to the PROJECT chat \u2014 use once at the end of an audit for the summary the team reads.",
10706
11314
  {
10707
- message: z16.string().min(1).max(2e4),
10708
- kind: z16.enum(["tag_audit_summary"]).optional().describe(
11315
+ message: z18.string().min(1).max(2e4),
11316
+ kind: z18.enum(["tag_audit_summary"]).optional().describe(
10709
11317
  "Set to 'tag_audit_summary' when posting a tag-audit summary so it is also saved to the persistent tag history"
10710
11318
  )
10711
11319
  },
@@ -10724,7 +11332,7 @@ function buildGetProjectTaskTool(connection, projectId) {
10724
11332
  "get_project_task",
10725
11333
  "Fetch any task in the project by id or slug: title, description, plan, status, and metadata. The audit evidence trail starts here.",
10726
11334
  {
10727
- taskId: z16.string().describe("Task id or slug")
11335
+ taskId: z18.string().describe("Task id or slug")
10728
11336
  },
10729
11337
  async ({ taskId }) => {
10730
11338
  try {
@@ -10742,8 +11350,8 @@ function buildReadProjectTaskChatTool(connection, projectId) {
10742
11350
  "read_project_task_chat",
10743
11351
  "Read any project task's chat messages (newest last). role 'user' rows are HUMAN turns; 'assistant'/'system' rows are agent posts and activity-log entries.",
10744
11352
  {
10745
- taskId: z16.string().describe("Task id or slug"),
10746
- limit: z16.number().int().min(1).max(200).optional().describe("Messages to fetch (default 50)")
11353
+ taskId: z18.string().describe("Task id or slug"),
11354
+ limit: z18.number().int().min(1).max(200).optional().describe("Messages to fetch (default 50)")
10747
11355
  },
10748
11356
  async ({ taskId, limit }) => {
10749
11357
  try {
@@ -10765,9 +11373,9 @@ function buildGetProjectTaskLogsTool(connection, projectId) {
10765
11373
  "get_project_task_logs",
10766
11374
  "Read any project task's persisted agent event stream (message / tool_use / turn_end / error / completed). Turn boundaries are turn_end events. Entries are truncated to ~2KB each; max 500 per call.",
10767
11375
  {
10768
- taskId: z16.string().describe("Task id or slug"),
10769
- limit: z16.number().int().min(1).max(500).optional().describe("Entries to fetch (default 50)"),
10770
- source: z16.enum(["agent", "application"]).optional().describe("Filter: 'agent' = model events (default useful for grading)")
11376
+ taskId: z18.string().describe("Task id or slug"),
11377
+ limit: z18.number().int().min(1).max(500).optional().describe("Entries to fetch (default 50)"),
11378
+ source: z18.enum(["agent", "application"]).optional().describe("Filter: 'agent' = model events (default useful for grading)")
10771
11379
  },
10772
11380
  async ({ taskId, limit, source }) => {
10773
11381
  try {
@@ -10785,44 +11393,44 @@ function buildGetProjectTaskLogsTool(connection, projectId) {
10785
11393
  { annotations: { readOnlyHint: true } }
10786
11394
  );
10787
11395
  }
10788
- var TURN_GRADE_SHAPE = z16.object({
10789
- turnIndex: z16.number().int().min(0),
10790
- phase: z16.enum(["planning", "building", "human"]),
10791
- grade: z16.enum(["correct", "neutral", "blunder"]),
10792
- reasoning: z16.string(),
10793
- eventType: z16.string().describe('e.g. "message", "tool_use", "human_message"'),
10794
- eventSummary: z16.string().max(200).describe("\u2264120 chars of what happened this turn")
11396
+ var TURN_GRADE_SHAPE = z18.object({
11397
+ turnIndex: z18.number().int().min(0),
11398
+ phase: z18.enum(["planning", "building", "human"]),
11399
+ grade: z18.enum(["correct", "neutral", "blunder"]),
11400
+ reasoning: z18.string(),
11401
+ eventType: z18.string().describe('e.g. "message", "tool_use", "human_message"'),
11402
+ eventSummary: z18.string().max(200).describe("\u2264120 chars of what happened this turn")
10795
11403
  });
10796
- var HUMAN_EVAL_SHAPE = z16.object({
10797
- messageIndex: z16.number().int().min(0).describe("Index into the task's human messages, oldest first"),
10798
- rating: z16.number().int().min(-1).max(1),
10799
- reasoning: z16.string()
11404
+ var HUMAN_EVAL_SHAPE = z18.object({
11405
+ messageIndex: z18.number().int().min(0).describe("Index into the task's human messages, oldest first"),
11406
+ rating: z18.number().int().min(-1).max(1),
11407
+ reasoning: z18.string()
10800
11408
  });
10801
11409
  function buildReportTaskAuditResultTool(connection, projectId) {
10802
11410
  return defineTool(
10803
11411
  "report_task_audit_result",
10804
11412
  "Persist one audited task's grades (call once per task after grading it). Pass error instead to mark the audit failed when the evidence is unusable.",
10805
11413
  {
10806
- taskId: z16.string().describe("The audited task's id (NOT slug)"),
10807
- summary: z16.string().describe("3-6 sentences: what went well, what was wasted"),
10808
- turnGrades: z16.array(TURN_GRADE_SHAPE),
10809
- planningAccuracy: z16.number().min(0).max(1).nullable(),
10810
- buildingAccuracy: z16.number().min(0).max(1).nullable(),
10811
- humanAccuracy: z16.number().min(0).max(1).nullable(),
10812
- planningCorrect: z16.number().int().min(0),
10813
- planningNeutral: z16.number().int().min(0),
10814
- planningBlunder: z16.number().int().min(0),
10815
- buildingCorrect: z16.number().int().min(0),
10816
- buildingNeutral: z16.number().int().min(0),
10817
- buildingBlunder: z16.number().int().min(0),
10818
- humanCorrect: z16.number().int().min(0),
10819
- humanNeutral: z16.number().int().min(0),
10820
- humanBlunder: z16.number().int().min(0),
10821
- humanEvaluations: z16.array(HUMAN_EVAL_SHAPE).optional(),
10822
- suggestionIds: z16.array(z16.string()).describe("Suggestion ids filed for this task, or []"),
10823
- auditCostUsd: z16.number().nullable(),
10824
- model: z16.string().nullable().describe("The model you are running as"),
10825
- error: z16.string().optional().describe("Set ONLY to mark this task's audit failed")
11414
+ taskId: z18.string().describe("The audited task's id (NOT slug)"),
11415
+ summary: z18.string().describe("3-6 sentences: what went well, what was wasted"),
11416
+ turnGrades: z18.array(TURN_GRADE_SHAPE),
11417
+ planningAccuracy: z18.number().min(0).max(1).nullable(),
11418
+ buildingAccuracy: z18.number().min(0).max(1).nullable(),
11419
+ humanAccuracy: z18.number().min(0).max(1).nullable(),
11420
+ planningCorrect: z18.number().int().min(0),
11421
+ planningNeutral: z18.number().int().min(0),
11422
+ planningBlunder: z18.number().int().min(0),
11423
+ buildingCorrect: z18.number().int().min(0),
11424
+ buildingNeutral: z18.number().int().min(0),
11425
+ buildingBlunder: z18.number().int().min(0),
11426
+ humanCorrect: z18.number().int().min(0),
11427
+ humanNeutral: z18.number().int().min(0),
11428
+ humanBlunder: z18.number().int().min(0),
11429
+ humanEvaluations: z18.array(HUMAN_EVAL_SHAPE).optional(),
11430
+ suggestionIds: z18.array(z18.string()).describe("Suggestion ids filed for this task, or []"),
11431
+ auditCostUsd: z18.number().nullable(),
11432
+ model: z18.string().nullable().describe("The model you are running as"),
11433
+ error: z18.string().optional().describe("Set ONLY to mark this task's audit failed")
10826
11434
  },
10827
11435
  async (input) => {
10828
11436
  try {
@@ -10879,21 +11487,13 @@ function buildProjectTools(connection, projectId, workspaceDir) {
10879
11487
  }
10880
11488
 
10881
11489
  // src/tools/drive-tools.ts
10882
- import { z as z17 } from "zod";
10883
- var MAX_CONTENT_CHARS = 1e6;
10884
11490
  var MAX_READ_CHARS = 1e5;
10885
11491
  function errText2(prefix, error) {
10886
11492
  return textResult(`${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`);
10887
11493
  }
10888
11494
  function buildDriveListFilesTool(connection, projectId) {
10889
- return defineTool(
10890
- "drive_list_files",
10891
- "List files and folders in the project's connected Google Drive folder. Omit folderId to list the project's root folder. Returns id, name, mimeType, size, and modified time for each entry.",
10892
- {
10893
- folderId: z17.string().optional().describe("Folder to list. Defaults to the project's connected root folder."),
10894
- search: z17.string().max(200).optional().describe("Only return names containing this text"),
10895
- limit: z17.number().int().min(1).max(200).optional().describe("Max entries (default 100)")
10896
- },
11495
+ return defineContractTool(
11496
+ driveListFilesContract,
10897
11497
  async ({ folderId, search, limit }) => {
10898
11498
  try {
10899
11499
  const result = await connection.call("listProjectDriveFiles", {
@@ -10911,10 +11511,8 @@ function buildDriveListFilesTool(connection, projectId) {
10911
11511
  );
10912
11512
  }
10913
11513
  function buildDriveReadFileTool(connection, projectId) {
10914
- return defineTool(
10915
- "drive_read_file",
10916
- "Read a file's text content from the project's connected Google Drive folder. Google Docs, Sheets, and Slides are exported to text automatically. Content over 100 KB is truncated.",
10917
- { fileId: z17.string().describe("Drive file id, as returned by drive_list_files") },
11514
+ return defineContractTool(
11515
+ driveReadFileContract,
10918
11516
  async ({ fileId }) => {
10919
11517
  try {
10920
11518
  const result = await connection.call("readProjectDriveFile", { projectId, fileId });
@@ -10936,15 +11534,8 @@ ${content}`);
10936
11534
  );
10937
11535
  }
10938
11536
  function buildDriveCreateFileTool(connection, projectId) {
10939
- return defineTool(
10940
- "drive_create_file",
10941
- "Create a new file in the project's connected Google Drive folder. Use drive_update_file to change an existing file instead.",
10942
- {
10943
- name: z17.string().min(1).max(255).describe("File name, without any path separators"),
10944
- content: z17.string().max(MAX_CONTENT_CHARS).describe("File content, UTF-8 text"),
10945
- mimeType: z17.string().optional().describe("MIME type (default text/plain)"),
10946
- folderId: z17.string().optional().describe("Destination folder. Defaults to the project's connected root folder.")
10947
- },
11537
+ return defineContractTool(
11538
+ driveCreateFileContract,
10948
11539
  async ({ name, content, mimeType, folderId }) => {
10949
11540
  try {
10950
11541
  const file = await connection.call("createProjectDriveFile", {
@@ -10962,14 +11553,8 @@ function buildDriveCreateFileTool(connection, projectId) {
10962
11553
  );
10963
11554
  }
10964
11555
  function buildDriveUpdateFileTool(connection, projectId) {
10965
- return defineTool(
10966
- "drive_update_file",
10967
- "Replace the content of an existing file in the project's connected Google Drive folder. This overwrites the whole file. Google-native documents cannot be overwritten.",
10968
- {
10969
- fileId: z17.string().describe("Drive file id, as returned by drive_list_files"),
10970
- content: z17.string().max(MAX_CONTENT_CHARS).describe("Replacement content, UTF-8 text"),
10971
- mimeType: z17.string().optional().describe("MIME type (defaults to the file's current type)")
10972
- },
11556
+ return defineContractTool(
11557
+ driveUpdateFileContract,
10973
11558
  async ({ fileId, content, mimeType }) => {
10974
11559
  try {
10975
11560
  const file = await connection.call("updateProjectDriveFile", {
@@ -10986,10 +11571,8 @@ function buildDriveUpdateFileTool(connection, projectId) {
10986
11571
  );
10987
11572
  }
10988
11573
  function buildDriveDeleteFileTool(connection, projectId) {
10989
- return defineTool(
10990
- "drive_delete_file",
10991
- "Move a file in the project's connected Google Drive folder to the Drive trash. The file is recoverable from the trash; it is never permanently deleted.",
10992
- { fileId: z17.string().describe("Drive file id, as returned by drive_list_files") },
11574
+ return defineContractTool(
11575
+ driveDeleteFileContract,
10993
11576
  async ({ fileId }) => {
10994
11577
  try {
10995
11578
  const result = await connection.call("deleteProjectDriveFile", { projectId, fileId });
@@ -11001,13 +11584,8 @@ function buildDriveDeleteFileTool(connection, projectId) {
11001
11584
  );
11002
11585
  }
11003
11586
  function buildDriveCreateFolderTool(connection, projectId) {
11004
- return defineTool(
11005
- "drive_create_folder",
11006
- "Create a folder inside the project's connected Google Drive folder.",
11007
- {
11008
- name: z17.string().min(1).max(255).describe("Folder name, without any path separators"),
11009
- folderId: z17.string().optional().describe("Parent folder. Defaults to the project's connected root folder.")
11010
- },
11587
+ return defineContractTool(
11588
+ driveCreateFolderContract,
11011
11589
  async ({ name, folderId }) => {
11012
11590
  try {
11013
11591
  const folder = await connection.call("createProjectDriveFolder", {
@@ -11033,10 +11611,209 @@ function buildDriveTools(connection, projectId) {
11033
11611
  ];
11034
11612
  }
11035
11613
 
11614
+ // src/tools/integration-tools.ts
11615
+ function errText3(prefix, error) {
11616
+ return textResult(`${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`);
11617
+ }
11618
+ function buildListProjectIntegrationsTool(connection, projectId) {
11619
+ return defineContractTool(listProjectIntegrationsContract, async () => {
11620
+ try {
11621
+ const res = await connection.call("listProjectIntegrations", { projectId });
11622
+ return textResult(JSON.stringify(res, null, 2));
11623
+ } catch (error) {
11624
+ return errText3("Failed to list integrations", error);
11625
+ }
11626
+ });
11627
+ }
11628
+ function buildListProjectChannelsTool(connection, projectId) {
11629
+ return defineContractTool(listProjectChannelsContract, async () => {
11630
+ try {
11631
+ const channels = await connection.call("listProjectChannels", { projectId });
11632
+ if (channels.length === 0) {
11633
+ return textResult(
11634
+ "No work channels are registered for this project. An admin registers them in project settings; until then no channel is readable, whatever the bot can see in the workspace."
11635
+ );
11636
+ }
11637
+ return textResult(JSON.stringify(channels, null, 2));
11638
+ } catch (error) {
11639
+ return errText3("Failed to list work channels", error);
11640
+ }
11641
+ });
11642
+ }
11643
+ function buildReadChannelMessagesTool(connection, projectId) {
11644
+ return defineContractTool(readChannelMessagesContract, async (input) => {
11645
+ try {
11646
+ const res = await connection.call("readChannelMessages", {
11647
+ projectId,
11648
+ channelId: input.channelId,
11649
+ limit: input.limit,
11650
+ before: input.before,
11651
+ after: input.after,
11652
+ threadTs: input.threadTs
11653
+ });
11654
+ return textResult(JSON.stringify(res, null, 2));
11655
+ } catch (error) {
11656
+ return errText3("Failed to read channel messages", error);
11657
+ }
11658
+ });
11659
+ }
11660
+ function buildPostChannelMessageTool(connection, projectId) {
11661
+ return defineContractTool(postChannelMessageContract, async (input) => {
11662
+ try {
11663
+ const res = await connection.call("postChannelMessage", {
11664
+ projectId,
11665
+ channelId: input.channelId,
11666
+ text: input.text,
11667
+ threadTs: input.threadTs
11668
+ });
11669
+ return textResult(`Posted to ${res.channelId} (message ${res.messageId}).`);
11670
+ } catch (error) {
11671
+ return errText3("Failed to post to channel", error);
11672
+ }
11673
+ });
11674
+ }
11675
+ function buildAnalyticsSummaryTool(connection, projectId) {
11676
+ return defineContractTool(getAnalyticsSummaryContract, async (input) => {
11677
+ try {
11678
+ const res = await connection.call("getProjectAnalyticsSummary", {
11679
+ projectId,
11680
+ rangeDays: input.rangeDays,
11681
+ campaign: input.campaign
11682
+ });
11683
+ if (!res.configured) {
11684
+ return textResult(res.message ?? "Google Analytics is not configured for this project.");
11685
+ }
11686
+ return textResult(JSON.stringify(res, null, 2));
11687
+ } catch (error) {
11688
+ return errText3("Failed to read the analytics summary", error);
11689
+ }
11690
+ });
11691
+ }
11692
+ function buildChannelTools(connection, projectId) {
11693
+ return [
11694
+ buildListProjectChannelsTool(connection, projectId),
11695
+ buildReadChannelMessagesTool(connection, projectId),
11696
+ buildPostChannelMessageTool(connection, projectId)
11697
+ ];
11698
+ }
11699
+
11700
+ // src/tools/log-tools.ts
11701
+ function portsFor(connection, projectId) {
11702
+ return {
11703
+ queryGcpLogs: (params) => connection.call("queryProjectGcpLogs", { ...params, projectId }),
11704
+ queryGrafanaLogs: (params) => connection.call("queryProjectGrafanaLogs", { ...params, projectId })
11705
+ };
11706
+ }
11707
+ function buildQueryGcpLogsTool(connection, projectId) {
11708
+ return defineContractTool(queryGcpLogsContract, async (input) => {
11709
+ try {
11710
+ return textResult(await runQueryGcpLogs(portsFor(connection, projectId), input));
11711
+ } catch (error) {
11712
+ return textResult(
11713
+ `Failed to query GCP logs: ${error instanceof Error ? error.message : "Unknown error"}`
11714
+ );
11715
+ }
11716
+ });
11717
+ }
11718
+ function buildQueryGrafanaLogsTool(connection, projectId) {
11719
+ return defineContractTool(queryGrafanaLogsContract, async (input) => {
11720
+ try {
11721
+ return textResult(await runQueryGrafanaLogs(portsFor(connection, projectId), input));
11722
+ } catch (error) {
11723
+ return textResult(
11724
+ `Failed to query Grafana logs: ${error instanceof Error ? error.message : "Unknown error"}`
11725
+ );
11726
+ }
11727
+ });
11728
+ }
11729
+
11730
+ // src/tools/meeting-tools.ts
11731
+ function errText4(prefix, error) {
11732
+ return textResult(`${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`);
11733
+ }
11734
+ function buildMeetingTools(connection, projectId) {
11735
+ return [
11736
+ defineContractTool(
11737
+ listMeetingsContract,
11738
+ async (input) => {
11739
+ try {
11740
+ const res = await connection.call("listMeetings", {
11741
+ projectId,
11742
+ limit: input.limit,
11743
+ search: input.search
11744
+ });
11745
+ if (res.meetings.length === 0) {
11746
+ return textResult(
11747
+ input.search ? `No meetings match "${input.search}". Try list_meetings with no search to see what exists.` : "This project has no meetings yet. They are added by pasting a transcript in the Meetings tab, or from Slack."
11748
+ );
11749
+ }
11750
+ return textResult(JSON.stringify(res.meetings, null, 2));
11751
+ } catch (error) {
11752
+ return errText4("Failed to list meetings", error);
11753
+ }
11754
+ },
11755
+ { annotations: { readOnlyHint: true } }
11756
+ ),
11757
+ defineContractTool(
11758
+ getMeetingContract,
11759
+ async (input) => {
11760
+ try {
11761
+ const res = await connection.call("getMeeting", {
11762
+ projectId,
11763
+ meetingId: input.meetingId
11764
+ });
11765
+ return textResult(JSON.stringify(res, null, 2));
11766
+ } catch (error) {
11767
+ return errText4("Failed to read the meeting", error);
11768
+ }
11769
+ },
11770
+ { annotations: { readOnlyHint: true } }
11771
+ ),
11772
+ defineContractTool(
11773
+ readMeetingTranscriptContract,
11774
+ async (input) => {
11775
+ try {
11776
+ const res = await connection.call("readMeetingTranscript", {
11777
+ projectId,
11778
+ meetingId: input.meetingId,
11779
+ offset: input.offset,
11780
+ limit: input.limit
11781
+ });
11782
+ const header = `${res.title} \u2014 segments ${res.offset + 1}-${res.offset + res.lines.length} of ${res.segmentCount}`;
11783
+ const footer = res.nextOffset === void 0 ? "" : `
11784
+
11785
+ -- more remains: pass offset=${res.nextOffset} to continue`;
11786
+ return textResult(`${header}
11787
+
11788
+ ${res.lines.join("\n")}${footer}`);
11789
+ } catch (error) {
11790
+ return errText4("Failed to read the transcript", error);
11791
+ }
11792
+ },
11793
+ { annotations: { readOnlyHint: true } }
11794
+ )
11795
+ ];
11796
+ }
11797
+
11798
+ // src/tools/connected-tools.ts
11799
+ function connectedToolsFor(connection, context) {
11800
+ const projectId = context?.projectId;
11801
+ if (!projectId) return [];
11802
+ return [
11803
+ ...context.googleDriveConnected ? buildDriveTools(connection, projectId) : [],
11804
+ buildListProjectIntegrationsTool(connection, projectId),
11805
+ ...context.chatChannelsConfigured ? buildChannelTools(connection, projectId) : [],
11806
+ ...context.meetingsAvailable ? buildMeetingTools(connection, projectId) : [],
11807
+ ...context.googleAnalyticsConfigured ? [buildAnalyticsSummaryTool(connection, projectId)] : [],
11808
+ ...context.gcpLogsConfigured ? [buildQueryGcpLogsTool(connection, projectId)] : [],
11809
+ ...context.grafanaLogsConfigured ? [buildQueryGrafanaLogsTool(connection, projectId)] : []
11810
+ ];
11811
+ }
11812
+
11036
11813
  // src/tools/code-review-tools.ts
11037
11814
  import { execFile } from "child_process";
11038
11815
  import { promisify } from "util";
11039
- import { z as z18 } from "zod";
11816
+ import { z as z19 } from "zod";
11040
11817
  async function endReviewSession(connection, reason) {
11041
11818
  await connection.call("endReviewSession", {
11042
11819
  sessionId: connection.sessionId,
@@ -11044,26 +11821,26 @@ async function endReviewSession(connection, reason) {
11044
11821
  });
11045
11822
  }
11046
11823
  var RISK_LEVELS2 = ["critical", "high", "medium", "low"];
11047
- var reviewedShaSchema = z18.string().regex(/^[0-9a-f]{40}$/i).describe("REQUIRED. The full 40-character commit SHA this verdict reviews.");
11824
+ var reviewedShaSchema = z19.string().regex(/^[0-9a-f]{40}$/i).describe("REQUIRED. The full 40-character commit SHA this verdict reviews.");
11048
11825
  var riskDescription = "REQUIRED. The risk level this change carries, judged by the surface area it touches: critical = touches critical/foundational surface, high = important surface, medium = moderate, low = small/isolated. Set this on every verdict. You have authority to override a risk level already set on the task if you disagree with it.";
11049
- var ReviewGuideToolSchema = z18.strictObject({
11050
- reviewedSha: z18.string().regex(/^[0-9a-f]{40}$/i).describe(
11826
+ var ReviewGuideToolSchema = z19.strictObject({
11827
+ reviewedSha: z19.string().regex(/^[0-9a-f]{40}$/i).describe(
11051
11828
  "REQUIRED. The PR's current head as a full 40-char SHA. Run `git rev-parse HEAD` immediately before this call \u2014 never extend an abbreviated hash into 40 characters."
11052
11829
  ),
11053
- overview: z18.string().min(1).max(6e4).describe("REQUIRED. Plain-text walkthrough intro, max 3000 characters. Keep it short."),
11054
- sections: z18.array(
11055
- z18.strictObject({
11056
- title: z18.string().min(1).max(160),
11057
- explanation: z18.string().min(1).max(2e3),
11058
- classification: z18.enum(["core", "supporting"]).optional(),
11059
- files: z18.array(
11060
- z18.strictObject({
11061
- path: z18.string().min(1).max(500).describe(
11830
+ overview: z19.string().min(1).max(6e4).describe("REQUIRED. Plain-text walkthrough intro, max 3000 characters. Keep it short."),
11831
+ sections: z19.array(
11832
+ z19.strictObject({
11833
+ title: z19.string().min(1).max(160),
11834
+ explanation: z19.string().min(1).max(2e3),
11835
+ classification: z19.enum(["core", "supporting"]).optional(),
11836
+ files: z19.array(
11837
+ z19.strictObject({
11838
+ path: z19.string().min(1).max(500).describe(
11062
11839
  "A file the PR's diff actually changed. Context files you merely read are rejected."
11063
11840
  ),
11064
- startLine: z18.number().int().positive().max(1e6).optional(),
11065
- endLine: z18.number().int().positive().max(1e6).optional(),
11066
- hunkHeader: z18.string().min(1).max(300).optional().describe(
11841
+ startLine: z19.number().int().positive().max(1e6).optional(),
11842
+ endLine: z19.number().int().positive().max(1e6).optional(),
11843
+ hunkHeader: z19.string().min(1).max(300).optional().describe(
11067
11844
  "Optional anchor, matched byte-exactly against the full hunk header line from `git diff` INCLUDING the context text after the second @@. Copy it verbatim from `git diff <base>..HEAD -- <file> | grep '^@@'`, or omit anchors entirely (path-only entries always validate)."
11068
11845
  )
11069
11846
  })
@@ -11147,8 +11924,8 @@ function buildApproveCodeReviewTool(connection) {
11147
11924
  "Approve the code review and exit. Use when the diff passes all review criteria. Requires a summary and a risk level \u2014 for changes, use request_code_changes with a structured issues[] list.",
11148
11925
  {
11149
11926
  reviewedSha: reviewedShaSchema,
11150
- summary: z18.string().describe("Brief summary of what was reviewed and why it looks good"),
11151
- risk: z18.enum(RISK_LEVELS2).describe(riskDescription)
11927
+ summary: z19.string().describe("Brief summary of what was reviewed and why it looks good"),
11928
+ risk: z19.enum(RISK_LEVELS2).describe(riskDescription)
11152
11929
  },
11153
11930
  async ({ reviewedSha, summary, risk }) => {
11154
11931
  const content = `**Code Review: Approved** :white_check_mark:
@@ -11179,16 +11956,16 @@ function buildRequestCodeChangesTool(connection) {
11179
11956
  "Request changes during code review and exit. Use when substantive issues must be fixed before merge. Each issue: { file, line?, severity: critical|major|minor, description }.",
11180
11957
  {
11181
11958
  reviewedSha: reviewedShaSchema,
11182
- issues: z18.array(
11183
- z18.object({
11184
- file: z18.string().describe("File path where the issue was found"),
11185
- line: z18.number().optional().describe("Line number (if applicable)"),
11186
- severity: z18.enum(["critical", "major", "minor"]).describe("Issue severity"),
11187
- description: z18.string().describe("What is wrong and how to fix it")
11959
+ issues: z19.array(
11960
+ z19.object({
11961
+ file: z19.string().describe("File path where the issue was found"),
11962
+ line: z19.number().optional().describe("Line number (if applicable)"),
11963
+ severity: z19.enum(["critical", "major", "minor"]).describe("Issue severity"),
11964
+ description: z19.string().describe("What is wrong and how to fix it")
11188
11965
  })
11189
11966
  ).describe("List of issues found during review"),
11190
- summary: z18.string().describe("Brief overall summary of the review findings"),
11191
- risk: z18.enum(RISK_LEVELS2).describe(riskDescription)
11967
+ summary: z19.string().describe("Brief overall summary of the review findings"),
11968
+ risk: z19.enum(RISK_LEVELS2).describe(riskDescription)
11192
11969
  },
11193
11970
  async ({ reviewedSha, issues, summary, risk }) => {
11194
11971
  const issueLines = issues.map((issue) => {
@@ -11237,26 +12014,30 @@ function getTaskModeTools(agentMode, connection) {
11237
12014
  }
11238
12015
  function getModeTools(agentMode, connection, config, context) {
11239
12016
  if (config.mode === "pack") {
11240
- return buildPmTools(connection, { includePackTools: true });
12017
+ return buildPmTools(connection, {
12018
+ includePackTools: config.packExecution === "fan-out"
12019
+ });
11241
12020
  }
11242
12021
  if (config.mode === "task") return getTaskModeTools(agentMode, connection);
12022
+ const packToolsFor = (isParent) => ({
12023
+ includePackTools: !!isParent && config.packExecution === "fan-out"
12024
+ });
11243
12025
  switch (agentMode) {
11244
12026
  case "building":
11245
- return context?.isParentTask ? buildPmTools(connection, { includePackTools: true }) : [];
12027
+ return context?.isParentTask ? buildPmTools(connection, packToolsFor(true)) : [];
11246
12028
  case "review":
11247
12029
  case "auto":
11248
12030
  case "discovery":
11249
12031
  case "help":
11250
- return buildPmTools(connection, {
11251
- includePackTools: !!context?.isParentTask
11252
- });
12032
+ return buildPmTools(connection, packToolsFor(context?.isParentTask));
11253
12033
  default:
11254
12034
  return config.mode === "pm" ? buildPmTools(connection, { includePackTools: false }) : [];
11255
12035
  }
11256
12036
  }
11257
12037
  function buildPrGuideToolsFor(effectiveMode, connection, config, context) {
11258
12038
  const isLeafBuild = effectiveMode === "building" || effectiveMode === "auto";
11259
- return isLeafBuild && !context?.isParentTask ? [
12039
+ const isSinglePodPack = config.mode === "pack" && config.packExecution !== "fan-out";
12040
+ return (isLeafBuild || isSinglePodPack) && (isSinglePodPack || !context?.isParentTask) ? [
11260
12041
  buildPublishReviewGuideTool(connection, {
11261
12042
  resolveHeadSha: () => resolveGitHeadSha(config.workspaceDir)
11262
12043
  })
@@ -11268,11 +12049,30 @@ var ALWAYS_LOADED_TOOLS = /* @__PURE__ */ new Set([
11268
12049
  "read_task_chat",
11269
12050
  "get_task",
11270
12051
  "get_current_plan",
12052
+ // The identity call the shared skills open with. Its schema is empty, so it
12053
+ // is the cheapest possible entry here, and deferring it would put a
12054
+ // ToolSearch round trip in front of the FIRST call of every skill run.
12055
+ "get_connection_context",
11271
12056
  "set_manual_tests",
11272
12057
  "create_pull_request",
11273
- // Planning/auto/building
11274
- "update_task_plan",
12058
+ // Planning/auto/building.
12059
+ //
12060
+ // A2-6, executed HALFWAY on purpose. The note here used to say the split pair
12061
+ // should leave this set once the prompts stopped naming them. `update_task`
12062
+ // has now replaced every `update_task_plan` reference in the prompts, so that
12063
+ // one is gone from the always-loaded set — it stays REGISTERED, just not hot.
12064
+ //
12065
+ // `update_task_properties` does NOT leave, and the plan that asked for the
12066
+ // pair to go was wrong about it: `update_task`'s agent surface deliberately
12067
+ // omits `storyPointValue` and `risk` (see tool-contracts/task-update.ts), and
12068
+ // this is the only pod tool that carries them. Discovery's ExitPlanMode gate
12069
+ // fails until the card has story points, risk AND a title, so dropping this
12070
+ // would leave the one mode that must set them with no tool that can — the
12071
+ // unfollowable-instruction defect, manufactured deliberately.
11275
12072
  "update_task_properties",
12073
+ // The shared-skill card write: one name whose fields match the mcp surface,
12074
+ // so a single skill text drives a local session and a pod verbatim.
12075
+ "update_task",
11276
12076
  // Building/auto — the PR guide is published as part of opening the PR
11277
12077
  "publish_review_guide",
11278
12078
  // Review mode
@@ -11298,12 +12098,9 @@ var PACK_PROMOTED_TOOLS = /* @__PURE__ */ new Set(["get_execution_logs"]);
11298
12098
  function glossaryToolsFor(connection, config, context) {
11299
12099
  return context?.projectId ? buildGlossaryTools(connection, context.projectId, config.taskId, config.workspaceDir) : [];
11300
12100
  }
11301
- function driveToolsFor(connection, context) {
11302
- return context?.projectId && context.googleDriveConnected ? buildDriveTools(connection, context.projectId) : [];
11303
- }
11304
- function promotedToolsFor(effectiveMode, isPack, isProjectAgent) {
12101
+ function promotedToolsFor(effectiveMode, isPack, isProjectAgent, isSinglePodPack = false) {
11305
12102
  const names = /* @__PURE__ */ new Set();
11306
- if (effectiveMode === "building" || effectiveMode === "auto") {
12103
+ if (effectiveMode === "building" || effectiveMode === "auto" || isSinglePodPack) {
11307
12104
  for (const name of BUILDING_PROMOTED_TOOLS) names.add(name);
11308
12105
  }
11309
12106
  if (effectiveMode === "review") {
@@ -11332,7 +12129,7 @@ function buildConveyorTools(connection, config, context, agentMode) {
11332
12129
  const handoffTools = config.mode === "pm" && (effectiveMode === "discovery" || effectiveMode === "auto") ? [buildHandoffTool(connection)] : [];
11333
12130
  const emergencyTools = [buildForceUpdateTaskStatusTool(connection)];
11334
12131
  const glossaryTools = glossaryToolsFor(connection, config, context);
11335
- const driveTools = driveToolsFor(connection, context);
12132
+ const connectedTools = connectedToolsFor(connection, context);
11336
12133
  const isPack = config.mode === "pack" || Boolean(context?.isParentTask);
11337
12134
  return withAlwaysLoad(
11338
12135
  [
@@ -11343,10 +12140,15 @@ function buildConveyorTools(connection, config, context, agentMode) {
11343
12140
  ...prGuideTools,
11344
12141
  ...handoffTools,
11345
12142
  ...glossaryTools,
11346
- ...driveTools,
12143
+ ...connectedTools,
11347
12144
  ...emergencyTools
11348
12145
  ],
11349
- promotedToolsFor(effectiveMode, isPack, config.mode === "pm")
12146
+ promotedToolsFor(
12147
+ effectiveMode,
12148
+ isPack,
12149
+ config.mode === "pm",
12150
+ config.mode === "pack" && config.packExecution !== "fan-out"
12151
+ )
11350
12152
  );
11351
12153
  }
11352
12154
  function createConveyorMcpServer(harness, connection, config, context, agentMode) {
@@ -11835,7 +12637,7 @@ function fallbackResetIso(rateLimitType, now = Date.now()) {
11835
12637
  // src/execution/task-property-utils.ts
11836
12638
  function collectMissingProps(taskProps) {
11837
12639
  const missing = [];
11838
- if (!taskProps.plan?.trim()) missing.push("plan (save via update_task_plan)");
12640
+ if (!taskProps.plan?.trim()) missing.push("plan (save via update_task)");
11839
12641
  if (!taskProps.storyPointId) missing.push("story points (use update_task_properties)");
11840
12642
  if (!taskProps.title || taskProps.title === "Untitled")
11841
12643
  missing.push("title (use update_task_properties)");
@@ -12013,7 +12815,12 @@ function matchesReadOnlyBlocked(cmd) {
12013
12815
  }
12014
12816
  return null;
12015
12817
  }
12818
+ var POST_CHANNEL_TOOL = /(^|__)post_channel_message$/;
12819
+ 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.";
12016
12820
  function handleReadOnlyToolAccess(toolName, input) {
12821
+ if (POST_CHANNEL_TOOL.test(toolName)) {
12822
+ return { behavior: "deny", message: CHANNEL_POST_DENIED };
12823
+ }
12017
12824
  if (PM_PLAN_FILE_TOOLS.has(toolName)) {
12018
12825
  if (isPlanFile(input)) {
12019
12826
  return { behavior: "allow", updatedInput: input };
@@ -12056,13 +12863,16 @@ function handleBuildingToolAccess(toolName, input) {
12056
12863
  return { behavior: "allow", updatedInput: input };
12057
12864
  }
12058
12865
  function handleReviewToolAccess(toolName, input) {
12866
+ if (POST_CHANNEL_TOOL.test(toolName)) {
12867
+ return { behavior: "deny", message: CHANNEL_POST_DENIED };
12868
+ }
12059
12869
  return handleBuildingToolAccess(toolName, input);
12060
12870
  }
12061
12871
  var CHAT_BLOCKED_BASH = /\bgit\s+push\b|\bgh\s+pr\b|\bhub\s+pull-request\b/;
12062
12872
  var CREATE_PR_TOOL = /(^|__)create_pull_request$/;
12063
12873
  var CHAT_PLAN_GATE_MESSAGE = [
12064
12874
  "This chat card has no saved plan, so it has no PR path yet.",
12065
- "If the conversation has turned into a development task, save a plan with update_task_plan first \u2014 that identifies the card and moves it to In Progress \u2014 then commit, push, and open the PR.",
12875
+ "If the conversation has turned into a development task, save a plan with update_task first \u2014 that identifies the card and moves it to In Progress \u2014 then commit, push, and open the PR.",
12066
12876
  "If it is still a conversation, deliver the work by attaching files to the card with upload_attachment instead."
12067
12877
  ].join(" ");
12068
12878
  function isChatPrTool(toolName, input) {
@@ -12092,7 +12902,7 @@ function enforceMissingProps(host, input, missingProps) {
12092
12902
  "Cannot exit plan mode. Required task properties are missing:",
12093
12903
  ...missingProps.map((p) => `- ${p}`),
12094
12904
  "",
12095
- "Fill these in using MCP tools (e.g. update_task_plan, update_task_properties), then call ExitPlanMode again.",
12905
+ "Fill these in using MCP tools (e.g. update_task, update_task_properties), then call ExitPlanMode again.",
12096
12906
  "",
12097
12907
  "If you have a deliberate reason to proceed without them, you must explicitly bypass validation by calling ExitPlanMode with `bypassValidation: true` as a tool argument. Do not bypass unless the team has asked you to \u2014 it will be surfaced in chat."
12098
12908
  ].join("\n")
@@ -12510,7 +13320,7 @@ async function buildFollowUpPrompt(host, context, followUpContent) {
12510
13320
  const followUpImages = typeof followUpContent === "string" ? [] : followUpContent.filter(
12511
13321
  (b) => b.type === "image"
12512
13322
  );
12513
- const textPrompt = isPmMode ? `${await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode)}
13323
+ const textPrompt = isPmMode ? `${await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode, host.config.packExecution)}
12514
13324
 
12515
13325
  ---
12516
13326
 
@@ -12654,6 +13464,9 @@ async function* watchForParkedTui(inner, host, opts) {
12654
13464
  clearTimeout(timer);
12655
13465
  }
12656
13466
  }
13467
+ function takesPlannerOpeningTurn(host, mode) {
13468
+ return host.config.mode === "plan" && mode === "discovery";
13469
+ }
12657
13470
  async function runSdkQuery(host, context, followUpContent, promptDeliveryOverride) {
12658
13471
  if (host.isStopped()) return;
12659
13472
  const mode = host.agentMode;
@@ -12681,7 +13494,10 @@ async function runSdkQuery(host, context, followUpContent, promptDeliveryOverrid
12681
13494
  await runFollowUpQuery(host, context, options, resume, followUpContent);
12682
13495
  return;
12683
13496
  }
12684
- if (isDiscoveryLike && (resume || host.harnessKind === "sdk")) {
13497
+ if (isDiscoveryLike && host.harnessKind === "sdk") {
13498
+ return;
13499
+ }
13500
+ if (isDiscoveryLike && resume && !takesPlannerOpeningTurn(host, mode)) {
12685
13501
  return;
12686
13502
  }
12687
13503
  await runInitialQuery(host, context, options, resume, promptDelivery);
@@ -12707,7 +13523,8 @@ async function runPrefilledFollowUp(host, context, options, resume, followUpCont
12707
13523
  host.config.mode,
12708
13524
  context,
12709
13525
  host.isAuto,
12710
- host.agentMode
13526
+ host.agentMode,
13527
+ host.config.packExecution
12711
13528
  );
12712
13529
  queryOptions.appendSystemPrompt = [queryOptions.appendSystemPrompt, initialPrompt].filter(Boolean).join("\n\n").slice(0, APPEND_SYSTEM_PROMPT_MAX_CHARS);
12713
13530
  }
@@ -12778,7 +13595,8 @@ async function runInitialQuery(host, context, options, resume, promptDelivery) {
12778
13595
  host.config.mode,
12779
13596
  context,
12780
13597
  host.isAuto,
12781
- host.agentMode
13598
+ host.agentMode,
13599
+ host.config.packExecution
12782
13600
  );
12783
13601
  const { prompt, appendSystemPrompt } = selectInitialPromptInput(
12784
13602
  promptDelivery,
@@ -12808,7 +13626,13 @@ async function buildRetryQuery(host, context, options, lastErrorWasImage) {
12808
13626
  );
12809
13627
  }
12810
13628
  const retryPrompt = buildMultimodalPrompt(
12811
- await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
13629
+ await buildInitialPrompt(
13630
+ host.config.mode,
13631
+ context,
13632
+ host.isAuto,
13633
+ host.agentMode,
13634
+ host.config.packExecution
13635
+ ),
12812
13636
  context,
12813
13637
  lastErrorWasImage || !supportsImageBlocks(host.harnessKind)
12814
13638
  );
@@ -12837,7 +13661,13 @@ async function handleAuthError(context, host, options) {
12837
13661
  context.claudeSessionId = null;
12838
13662
  host.connection.storeSessionId("");
12839
13663
  const freshPrompt = buildMultimodalPrompt(
12840
- await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
13664
+ await buildInitialPrompt(
13665
+ host.config.mode,
13666
+ context,
13667
+ host.isAuto,
13668
+ host.agentMode,
13669
+ host.config.packExecution
13670
+ ),
12841
13671
  context,
12842
13672
  !supportsImageBlocks(host.harnessKind)
12843
13673
  );
@@ -12852,7 +13682,13 @@ async function handleStaleSession(context, host, options) {
12852
13682
  context.claudeSessionId = null;
12853
13683
  host.connection.storeSessionId("");
12854
13684
  const freshPrompt = buildMultimodalPrompt(
12855
- await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
13685
+ await buildInitialPrompt(
13686
+ host.config.mode,
13687
+ context,
13688
+ host.isAuto,
13689
+ host.agentMode,
13690
+ host.config.packExecution
13691
+ ),
12856
13692
  context,
12857
13693
  !supportsImageBlocks(host.harnessKind)
12858
13694
  );
@@ -12946,7 +13782,13 @@ async function handleUsageCapRejection(context, host, options, rateLimitType, re
12946
13782
  context.claudeSessionId = null;
12947
13783
  host.connection.storeSessionId("");
12948
13784
  const freshPrompt = buildMultimodalPrompt(
12949
- await buildInitialPrompt(host.config.mode, context, host.isAuto, host.agentMode),
13785
+ await buildInitialPrompt(
13786
+ host.config.mode,
13787
+ context,
13788
+ host.isAuto,
13789
+ host.agentMode,
13790
+ host.config.packExecution
13791
+ ),
12950
13792
  context,
12951
13793
  !supportsImageBlocks(host.harnessKind)
12952
13794
  );
@@ -13778,9 +14620,20 @@ var BackgroundWorkTracker = class {
13778
14620
  }
13779
14621
  };
13780
14622
 
14623
+ // src/runner/live-children.ts
14624
+ function findLiveChild(sources) {
14625
+ for (const source of sources) {
14626
+ for (const sessionId of source.activeSessionIds()) {
14627
+ if (sessionId) return { kind: source.kind, sessionId };
14628
+ }
14629
+ }
14630
+ return null;
14631
+ }
14632
+
13781
14633
  // src/runner/session-runner.ts
13782
14634
  var AUTO_RUN_MODES = /* @__PURE__ */ new Set(["building", "auto", "review", "discovery", "chat"]);
13783
14635
  var AUTONOMOUS_RUNNER_MODES = /* @__PURE__ */ new Set(["pack", "pm", "code-review"]);
14636
+ var CHILD_DEFER_RECHECK_MS = 60 * 1e3;
13784
14637
  var SessionRunner = class _SessionRunner {
13785
14638
  connection;
13786
14639
  mode;
@@ -13852,6 +14705,7 @@ var SessionRunner = class _SessionRunner {
13852
14705
  this.connection.sendHeartbeat(this.loopLag.takeMaxLagMs(), loopStatus);
13853
14706
  },
13854
14707
  onIdleTimeout: () => {
14708
+ if (this.deferShutdownForLiveChild("idle")) return;
13855
14709
  process.stderr.write("[conveyor-agent] Idle timeout reached, stopping agent\n");
13856
14710
  this.stopped = true;
13857
14711
  this.queryBridge?.stop();
@@ -13862,6 +14716,7 @@ var SessionRunner = class _SessionRunner {
13862
14716
  }
13863
14717
  },
13864
14718
  onDormantTimeout: () => {
14719
+ if (this.deferShutdownForLiveChild("dormant")) return;
13865
14720
  process.stderr.write("[conveyor-agent] Dormant idle timeout reached, shutting down\n");
13866
14721
  this.stopped = true;
13867
14722
  this.queryBridge?.stop();
@@ -13879,6 +14734,58 @@ var SessionRunner = class _SessionRunner {
13879
14734
  get state() {
13880
14735
  return this._state;
13881
14736
  }
14737
+ /**
14738
+ * Supervisors whose live children keep this pod alive. Empty until `cli.ts`
14739
+ * wires them, which is deliberate: the supervisors are constructed AFTER the
14740
+ * runner, and an empty list simply means "no children to protect" — the
14741
+ * pre-existing shutdown behavior.
14742
+ */
14743
+ liveChildSources = [];
14744
+ /** Called by `cli.ts` once the child supervisors exist. */
14745
+ setLiveChildSources(sources) {
14746
+ this.liveChildSources = sources;
14747
+ }
14748
+ /**
14749
+ * Suppress an idle/dormant shutdown while a spawned child is still working.
14750
+ *
14751
+ * The Planner is the pod's main process, so its timeouts take the pod — and
14752
+ * the Builder's in-flight work — down with it. Returns true when the shutdown
14753
+ * was deferred and the caller must not proceed.
14754
+ *
14755
+ * Re-arms with a SHORT delay rather than a fresh full window: the pod should
14756
+ * converge on shutdown soon after the last child exits, not one more idle
14757
+ * window later. Bounded by the configured timeout so a test with a 50ms idle
14758
+ * window re-checks in 50ms rather than a minute.
14759
+ *
14760
+ * Fail-open by construction — if the probe throws, or no sources are wired,
14761
+ * the shutdown proceeds exactly as before. The opposite bias (a pod that
14762
+ * cannot die) is the more expensive mistake here only in money; killing a
14763
+ * live build costs work.
14764
+ */
14765
+ deferShutdownForLiveChild(timer) {
14766
+ let live;
14767
+ try {
14768
+ live = findLiveChild(this.liveChildSources);
14769
+ } catch (err) {
14770
+ process.stderr.write(`[conveyor-agent] Live-child probe failed, not deferring: ${err}
14771
+ `);
14772
+ return false;
14773
+ }
14774
+ if (!live) return false;
14775
+ const recheckMs = Math.min(this.lifecycle.config.idleTimeoutMs, CHILD_DEFER_RECHECK_MS);
14776
+ const label = timer === "idle" ? "Idle" : "Dormant idle";
14777
+ process.stderr.write(
14778
+ `[conveyor-agent] ${label} timeout deferred: ${live.kind} session ${live.sessionId} active
14779
+ `
14780
+ );
14781
+ if (timer === "idle") {
14782
+ this.lifecycle.startIdleTimer(recheckMs);
14783
+ } else {
14784
+ this.dormantDeadline = Date.now() + recheckMs;
14785
+ this.lifecycle.startDormantTimer(recheckMs);
14786
+ }
14787
+ return true;
14788
+ }
13882
14789
  get sessionId() {
13883
14790
  return this.connection.sessionId;
13884
14791
  }
@@ -14604,6 +15511,7 @@ var SessionRunner = class _SessionRunner {
14604
15511
  instructions: this.fullContext?.agentInstructions ?? "",
14605
15512
  workspaceDir: this.config.workspaceDir,
14606
15513
  mode: this.config.runnerMode,
15514
+ packExecution: this.config.packExecution,
14607
15515
  isAuto: this.config.isAuto
14608
15516
  };
14609
15517
  const bridge = new QueryBridge(this.connection, this.mode, runnerConfig, {
@@ -14867,6 +15775,8 @@ function unshallowRepo(workspaceDir) {
14867
15775
 
14868
15776
  export {
14869
15777
  DEFAULT_SONNET_MODEL,
15778
+ RUNNER_MODES,
15779
+ parsePackExecution,
14870
15780
  isPermissionDeniedError,
14871
15781
  buildSynthesizedCredentials,
14872
15782
  claudeJsonPath,