@rallycry/conveyor-agent 11.0.21 → 11.0.23

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.
@@ -217,6 +217,30 @@ async function runPipeline(steps, ctx) {
217
217
  );
218
218
  }
219
219
 
220
+ // src/boot/bootstrap-refusal.ts
221
+ var BootstrapRejectedError = class extends Error {
222
+ constructor(message, code) {
223
+ super(message);
224
+ this.code = code;
225
+ this.name = "BootstrapRejectedError";
226
+ }
227
+ code;
228
+ };
229
+ async function readRefusal(res) {
230
+ if (res.status < 400 || res.status >= 500) return void 0;
231
+ let body;
232
+ try {
233
+ body = JSON.parse(await res.text());
234
+ } catch {
235
+ return void 0;
236
+ }
237
+ const payload = body;
238
+ if (payload?.retryable !== false) return void 0;
239
+ const message = typeof payload.error === "string" ? payload.error : "bootstrap refused";
240
+ const code = typeof payload.code === "string" ? payload.code : void 0;
241
+ return new BootstrapRejectedError(`${message} (HTTP ${res.status})`, code);
242
+ }
243
+
220
244
  // src/boot/poll.ts
221
245
  var BootstrapUnauthorizedError = class extends Error {
222
246
  constructor(message) {
@@ -261,6 +285,10 @@ async function pollBootstrapBundle(apiUrl, token, deps) {
261
285
  if (res?.status === 401) {
262
286
  throw new BootstrapUnauthorizedError("bootstrap token rejected (401) \u2014 pod identity invalid");
263
287
  }
288
+ if (res) {
289
+ const refusal = await readRefusal(res);
290
+ if (refusal) throw refusal;
291
+ }
264
292
  pollCount++;
265
293
  if (pollCount % PROGRESS_LOG_EVERY === 0) {
266
294
  deps.log.info(`[boot] Still waiting for bind (poll #${pollCount})...`);
@@ -1446,7 +1446,7 @@ ${q.question}${q.options.length ? "\n" + q.options.map((o) => `- ${o.label}: ${o
1446
1446
  }
1447
1447
  };
1448
1448
 
1449
- // ../shared/dist/chunk-42BS7Y35.js
1449
+ // ../shared/dist/chunk-6NPOGQPP.js
1450
1450
  import { z } from "zod";
1451
1451
  var DEFAULT_SONNET_MODEL = "claude-sonnet-5";
1452
1452
  var DEFAULT_OPUS_MODEL = "claude-opus-5";
@@ -1709,6 +1709,8 @@ import { z as z7 } from "zod";
1709
1709
  import { z as z8 } from "zod";
1710
1710
  import { z as z9 } from "zod";
1711
1711
  import { z as z10 } from "zod";
1712
+ import { z as z11 } from "zod";
1713
+ import { z as z12 } from "zod";
1712
1714
  var EXTERNAL_AGENT_MESSAGE_SOURCE = "external_agent";
1713
1715
  var TUI_KINDS = ["claude-code", "opencode", "codex"];
1714
1716
  var ACHIEVEMENT_RARITIES = [
@@ -3235,135 +3237,291 @@ var CRITICAL_AUTOMATED_SOURCES = /* @__PURE__ */ new Set([
3235
3237
  // clear the completion guard.
3236
3238
  "ci_result"
3237
3239
  ]);
3240
+ var DECISION_STATUS_LABELS = ["Open", "Decided", "Cancelled"];
3241
+ var DECISION_OPTION_KINDS = ["current", "planned", "proposed"];
3242
+ var DECISION_VOTE_SOURCES = ["web", "slack", "discord", "agent"];
3243
+ var DECISION_OPTIONS_MIN = 2;
3244
+ var DECISION_OPTIONS_MAX = 3;
3245
+ var DECISION_OPTION_LABEL_MAX = 80;
3246
+ var DECISION_QUESTION_MAX = 200;
3247
+ var DECISION_RESOLUTION_MAX = CARD_DESCRIPTION_MAX;
3248
+ var DECISION_VOTE_REASON_MAX = CARD_DESCRIPTION_MAX;
3249
+ var DECISION_DEADLINE_MIN_HOURS = 1;
3250
+ var DECISION_DEADLINE_MAX_DAYS = 30;
3251
+ var DECISION_DEADLINE_MAX_HOURS = DECISION_DEADLINE_MAX_DAYS * 24;
3252
+ var DECISION_LIST_MAX_LIMIT = 200;
3253
+ var decisionIdSchema = z10.string().cuid("Invalid decision ID");
3254
+ var optionInputSchema = z10.object({
3255
+ label: z10.string().min(1, "Option label is required").max(DECISION_OPTION_LABEL_MAX),
3256
+ description: z10.string().min(1, "Describe the option as GIVEN / WHEN / THEN"),
3257
+ kind: z10.enum(DECISION_OPTION_KINDS).optional()
3258
+ });
3259
+ var optionsArraySchema = z10.array(optionInputSchema).min(DECISION_OPTIONS_MIN, `A decision needs at least ${DECISION_OPTIONS_MIN} options`).max(
3260
+ DECISION_OPTIONS_MAX,
3261
+ `A decision takes at most ${DECISION_OPTIONS_MAX} options \u2014 more than that means the question is not framed yet`
3262
+ );
3263
+ var CreateDecisionRequestSchema = z10.object({
3264
+ projectId: z10.string().cuid("Invalid project ID"),
3265
+ /** The question, as one plain sentence. */
3266
+ title: z10.string().min(1, "A decision needs a question").max(DECISION_QUESTION_MAX),
3267
+ description: z10.string().max(CARD_DESCRIPTION_MAX).optional(),
3268
+ context: z10.string().min(1, "Context is required \u2014 say why this is being asked now"),
3269
+ options: optionsArraySchema,
3270
+ /** 1-based position of the option that wins on silence. */
3271
+ defaultOptionOrdinal: z10.number().int().min(1).max(DECISION_OPTIONS_MAX),
3272
+ deadlineHours: z10.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional(),
3273
+ /** Cards this decision blocks, by cuid or slug. */
3274
+ impactedCardRefs: z10.array(z10.string().min(1)).max(50).optional(),
3275
+ tagNames: z10.array(z10.string().min(1)).max(10).optional(),
3276
+ raisedByTaskId: z10.string().min(1).optional(),
3277
+ raisedByPrNumber: z10.number().int().positive().optional()
3278
+ });
3279
+ var VoteDecisionRequestSchema = z10.object({
3280
+ decisionId: decisionIdSchema,
3281
+ optionId: z10.string().cuid("Invalid option ID"),
3282
+ reason: z10.string().max(DECISION_VOTE_REASON_MAX).optional(),
3283
+ source: z10.enum(DECISION_VOTE_SOURCES).optional()
3284
+ });
3285
+ var RemoveDecisionVoteRequestSchema = z10.object({
3286
+ decisionId: decisionIdSchema
3287
+ });
3288
+ var ResolveDecisionRequestSchema = z10.object({
3289
+ decisionId: decisionIdSchema,
3290
+ chosenOptionId: z10.string().cuid("Invalid option ID"),
3291
+ resolution: z10.string().min(1, "A resolution is required \u2014 say what was decided").max(DECISION_RESOLUTION_MAX),
3292
+ resolutionNotes: z10.string().optional()
3293
+ });
3294
+ var CancelDecisionRequestSchema = z10.object({
3295
+ decisionId: decisionIdSchema,
3296
+ /** Why it is being dropped. Posted to the decision thread. */
3297
+ reason: z10.string().max(DECISION_RESOLUTION_MAX).optional()
3298
+ });
3299
+ var ReopenDecisionRequestSchema = z10.object({
3300
+ decisionId: decisionIdSchema,
3301
+ deadlineHours: z10.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional()
3302
+ });
3303
+ var UpdateDecisionRequestSchema = z10.object({
3304
+ decisionId: decisionIdSchema,
3305
+ title: z10.string().min(1).max(200).optional(),
3306
+ description: z10.string().max(CARD_DESCRIPTION_MAX).optional(),
3307
+ context: z10.string().min(1).optional(),
3308
+ deadlineAt: z10.string().datetime().optional(),
3309
+ /** Only accepted while the decision has zero votes. */
3310
+ options: optionsArraySchema.optional(),
3311
+ defaultOptionOrdinal: z10.number().int().min(1).max(DECISION_OPTIONS_MAX).optional(),
3312
+ addImpactedCardRefs: z10.array(z10.string().min(1)).max(50).optional(),
3313
+ removeImpactedCardIds: z10.array(z10.string().min(1)).max(50).optional()
3314
+ });
3315
+ var GetDecisionRequestSchema = z10.object({
3316
+ /** cuid or slug. A slug needs `projectId` to resolve unambiguously. */
3317
+ decisionId: z10.string().min(1),
3318
+ projectId: z10.string().cuid().optional()
3319
+ });
3320
+ var ListDecisionsRequestSchema = z10.object({
3321
+ projectId: z10.string().cuid("Invalid project ID"),
3322
+ status: z10.enum(DECISION_STATUS_LABELS).optional(),
3323
+ /** Tag name, not id — an agent knows the tag it is working under. */
3324
+ tag: z10.string().min(1).optional(),
3325
+ /** Decisions that this card is blocked by (cuid or slug). */
3326
+ cardId: z10.string().min(1).optional(),
3327
+ query: z10.string().min(1).optional(),
3328
+ limit: z10.number().int().min(1).max(DECISION_LIST_MAX_LIMIT).optional()
3329
+ });
3330
+ var optionInput = z11.object({
3331
+ label: z11.string().min(1).max(DECISION_OPTION_LABEL_MAX),
3332
+ description: z11.string().min(1),
3333
+ kind: z11.enum(DECISION_OPTION_KINDS).optional()
3334
+ });
3335
+ var optionsInput = z11.array(optionInput).min(DECISION_OPTIONS_MIN).max(DECISION_OPTIONS_MAX);
3336
+ var deadlineHours = z11.number().min(DECISION_DEADLINE_MIN_HOURS).max(DECISION_DEADLINE_MAX_HOURS).optional();
3337
+ var CreateProjectDecisionRequestSchema = z11.object({
3338
+ projectId: z11.string(),
3339
+ question: z11.string().min(1).max(200),
3340
+ summary: z11.string().max(CARD_DESCRIPTION_MAX).optional(),
3341
+ context: z11.string().min(1),
3342
+ options: optionsInput,
3343
+ defaultOption: z11.number().int().min(1).max(DECISION_OPTIONS_MAX),
3344
+ deadlineHours,
3345
+ impactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3346
+ /**
3347
+ * Pod surface only: also block the card this session is working on. The
3348
+ * session's task id is resolved SERVER-side from `sessionId` — a
3349
+ * client-supplied card id would let a pod block any card in the project.
3350
+ */
3351
+ includeCurrentCard: z11.boolean().optional(),
3352
+ sessionId: z11.string().optional(),
3353
+ tagNames: z11.array(z11.string().min(1)).max(10).optional(),
3354
+ raisedByPr: z11.number().int().positive().optional(),
3355
+ requestingUserId: z11.string().optional()
3356
+ });
3357
+ var ListProjectDecisionsRequestSchema = z11.object({
3358
+ projectId: z11.string(),
3359
+ status: z11.enum(DECISION_STATUS_LABELS).optional(),
3360
+ tag: z11.string().min(1).optional(),
3361
+ card: z11.string().min(1).optional(),
3362
+ query: z11.string().min(1).optional(),
3363
+ limit: z11.number().int().min(1).max(DECISION_LIST_MAX_LIMIT).optional(),
3364
+ requestingUserId: z11.string().optional()
3365
+ });
3366
+ var GetProjectDecisionRequestSchema = z11.object({
3367
+ projectId: z11.string(),
3368
+ decision: z11.string().min(1),
3369
+ requestingUserId: z11.string().optional()
3370
+ });
3371
+ var VoteProjectDecisionRequestSchema = z11.object({
3372
+ projectId: z11.string(),
3373
+ decision: z11.string().min(1),
3374
+ /** An option id, or its 1-based position. */
3375
+ option: z11.string().min(1),
3376
+ reason: z11.string().max(CARD_DESCRIPTION_MAX).optional(),
3377
+ requestingUserId: z11.string().optional()
3378
+ });
3379
+ var ResolveProjectDecisionRequestSchema = z11.object({
3380
+ projectId: z11.string(),
3381
+ decision: z11.string().min(1),
3382
+ option: z11.string().min(1),
3383
+ resolution: z11.string().min(1).max(DECISION_RESOLUTION_MAX),
3384
+ notes: z11.string().optional(),
3385
+ requestingUserId: z11.string().optional()
3386
+ });
3387
+ var UpdateProjectDecisionRequestSchema = z11.object({
3388
+ projectId: z11.string(),
3389
+ decision: z11.string().min(1),
3390
+ deadlineHours,
3391
+ addImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3392
+ removeImpactedCards: z11.array(z11.string().min(1)).max(50).optional(),
3393
+ status: z11.enum(["Cancelled", "Open"]).optional(),
3394
+ requestingUserId: z11.string().optional()
3395
+ });
3238
3396
  var MEETING_CHECKLIST_TITLE_MAX = 300;
3239
3397
  var MEETING_TRANSCRIPT_MAX_CHARS = 2e6;
3240
3398
  var MEETING_TITLE_MAX = 200;
3241
3399
  var MEETING_OCCURRED_AT_MIN_YEAR = 2e3;
3242
3400
  var MEETING_OCCURRED_AT_MAX_FUTURE_MS = 48 * 60 * 60 * 1e3;
3243
3401
  var OCCURRED_AT_RANGE_MESSAGE = `occurredAt must be a real date: no earlier than ${MEETING_OCCURRED_AT_MIN_YEAR}, and no more than 48 hours in the future.`;
3244
- var MeetingOccurredAtSchema = z10.string().datetime().refine((value) => {
3402
+ var MeetingOccurredAtSchema = z12.string().datetime().refine((value) => {
3245
3403
  const ms = Date.parse(value);
3246
3404
  if (Number.isNaN(ms)) return false;
3247
3405
  if (ms > Date.now() + MEETING_OCCURRED_AT_MAX_FUTURE_MS) return false;
3248
3406
  return new Date(ms).getUTCFullYear() >= MEETING_OCCURRED_AT_MIN_YEAR;
3249
3407
  }, OCCURRED_AT_RANGE_MESSAGE);
3250
- var CreateMeetingFromTranscriptRequestSchema = z10.object({
3251
- projectId: z10.string().cuid(),
3252
- rawText: z10.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
3253
- title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
3408
+ var CreateMeetingFromTranscriptRequestSchema = z12.object({
3409
+ projectId: z12.string().cuid(),
3410
+ rawText: z12.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
3411
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
3254
3412
  /** ISO 8601. Defaults to now when the source carries no date. */
3255
3413
  occurredAt: MeetingOccurredAtSchema.optional(),
3256
3414
  /** Override auto-detection. Rarely needed; detection handles the three formats. */
3257
- format: z10.enum(["text", "vtt", "srt"]).optional(),
3258
- source: z10.enum(["manual", "slack"]).optional()
3415
+ format: z12.enum(["text", "vtt", "srt"]).optional(),
3416
+ source: z12.enum(["manual", "slack"]).optional()
3259
3417
  });
3260
- var GetMeetingRequestSchema = z10.object({
3261
- projectId: z10.string().cuid(),
3262
- meetingId: z10.string().cuid()
3418
+ var GetMeetingRequestSchema = z12.object({
3419
+ projectId: z12.string().cuid(),
3420
+ meetingId: z12.string().cuid()
3263
3421
  });
3264
- var UpdateMeetingRequestSchema = z10.object({
3265
- projectId: z10.string().cuid(),
3266
- meetingId: z10.string().cuid(),
3267
- title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
3422
+ var UpdateMeetingRequestSchema = z12.object({
3423
+ projectId: z12.string().cuid(),
3424
+ meetingId: z12.string().cuid(),
3425
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
3268
3426
  occurredAt: MeetingOccurredAtSchema.optional()
3269
3427
  });
3270
- var RegenerateMeetingSummaryRequestSchema = z10.object({
3271
- projectId: z10.string().cuid(),
3272
- meetingId: z10.string().cuid()
3428
+ var RegenerateMeetingSummaryRequestSchema = z12.object({
3429
+ projectId: z12.string().cuid(),
3430
+ meetingId: z12.string().cuid()
3273
3431
  });
3274
- var DeleteMeetingRequestSchema = z10.object({
3275
- projectId: z10.string().cuid(),
3276
- meetingId: z10.string().cuid()
3432
+ var DeleteMeetingRequestSchema = z12.object({
3433
+ projectId: z12.string().cuid(),
3434
+ meetingId: z12.string().cuid()
3277
3435
  });
3278
- var checklistTitle = z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX);
3279
- var ListMeetingChecklistRequestSchema = z10.object({
3280
- projectId: z10.string().cuid(),
3281
- meetingId: z10.string().cuid()
3436
+ var checklistTitle = z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX);
3437
+ var ListMeetingChecklistRequestSchema = z12.object({
3438
+ projectId: z12.string().cuid(),
3439
+ meetingId: z12.string().cuid()
3282
3440
  });
3283
- var AddMeetingChecklistItemsRequestSchema = z10.object({
3284
- projectId: z10.string().cuid(),
3285
- meetingId: z10.string().cuid(),
3286
- items: z10.array(z10.object({ title: checklistTitle })).min(1).max(50)
3441
+ var AddMeetingChecklistItemsRequestSchema = z12.object({
3442
+ projectId: z12.string().cuid(),
3443
+ meetingId: z12.string().cuid(),
3444
+ items: z12.array(z12.object({ title: checklistTitle })).min(1).max(50)
3287
3445
  });
3288
- var UpdateMeetingChecklistItemRequestSchema = z10.object({
3289
- projectId: z10.string().cuid(),
3290
- meetingId: z10.string().cuid(),
3291
- itemId: z10.string().cuid(),
3446
+ var UpdateMeetingChecklistItemRequestSchema = z12.object({
3447
+ projectId: z12.string().cuid(),
3448
+ meetingId: z12.string().cuid(),
3449
+ itemId: z12.string().cuid(),
3292
3450
  title: checklistTitle.optional(),
3293
- ordinal: z10.number().int().min(0).optional(),
3451
+ ordinal: z12.number().int().min(0).optional(),
3294
3452
  /** Explicit null clears the link; undefined leaves it alone. */
3295
- linkedTaskId: z10.string().cuid().nullable().optional()
3453
+ linkedTaskId: z12.string().cuid().nullable().optional()
3296
3454
  }).refine(
3297
3455
  (v) => v.title !== void 0 || v.ordinal !== void 0 || v.linkedTaskId !== void 0,
3298
3456
  "Pass at least one of title, ordinal, or linkedTaskId."
3299
3457
  );
3300
- var DeleteMeetingChecklistItemRequestSchema = z10.object({
3301
- projectId: z10.string().cuid(),
3302
- meetingId: z10.string().cuid(),
3303
- itemId: z10.string().cuid()
3304
- });
3305
- var SetMeetingChecklistItemCheckedRequestSchema = z10.object({
3306
- projectId: z10.string().cuid(),
3307
- meetingId: z10.string().cuid(),
3308
- itemId: z10.string().cuid(),
3309
- checked: z10.boolean(),
3458
+ var DeleteMeetingChecklistItemRequestSchema = z12.object({
3459
+ projectId: z12.string().cuid(),
3460
+ meetingId: z12.string().cuid(),
3461
+ itemId: z12.string().cuid()
3462
+ });
3463
+ var SetMeetingChecklistItemCheckedRequestSchema = z12.object({
3464
+ projectId: z12.string().cuid(),
3465
+ meetingId: z12.string().cuid(),
3466
+ itemId: z12.string().cuid(),
3467
+ checked: z12.boolean(),
3310
3468
  /** Attach the card in the same call that ticks the item. */
3311
- linkedTaskId: z10.string().cuid().nullable().optional()
3469
+ linkedTaskId: z12.string().cuid().nullable().optional()
3312
3470
  });
3313
- var ListMeetingsRequestSchema = z10.object({
3314
- projectId: z10.string().cuid(),
3315
- limit: z10.number().int().min(1).max(50).optional(),
3316
- search: z10.string().max(200).optional()
3471
+ var ListMeetingsRequestSchema = z12.object({
3472
+ projectId: z12.string().cuid(),
3473
+ limit: z12.number().int().min(1).max(50).optional(),
3474
+ search: z12.string().max(200).optional()
3317
3475
  });
3318
- var ReadMeetingTranscriptRequestSchema = z10.object({
3319
- projectId: z10.string().cuid(),
3320
- meetingId: z10.string().cuid(),
3321
- offset: z10.number().int().min(0).optional(),
3322
- limit: z10.number().int().min(1).max(500).optional()
3476
+ var ReadMeetingTranscriptRequestSchema = z12.object({
3477
+ projectId: z12.string().cuid(),
3478
+ meetingId: z12.string().cuid(),
3479
+ offset: z12.number().int().min(0).optional(),
3480
+ limit: z12.number().int().min(1).max(500).optional()
3323
3481
  });
3324
3482
  var MEETING_SUMMARY_MAX_CHARS = 5e4;
3325
- var AddProjectMeetingChecklistItemsRequestSchema = z10.object({
3326
- projectId: z10.string().cuid(),
3327
- meetingId: z10.string().cuid(),
3328
- items: z10.array(z10.object({ title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX) })).min(1).max(50),
3329
- requestingUserId: z10.string().optional()
3330
- });
3331
- var CheckProjectMeetingChecklistItemRequestSchema = z10.object({
3332
- projectId: z10.string().cuid(),
3333
- meetingId: z10.string().cuid(),
3334
- title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3335
- checked: z10.boolean(),
3483
+ var AddProjectMeetingChecklistItemsRequestSchema = z12.object({
3484
+ projectId: z12.string().cuid(),
3485
+ meetingId: z12.string().cuid(),
3486
+ items: z12.array(z12.object({ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX) })).min(1).max(50),
3487
+ requestingUserId: z12.string().optional()
3488
+ });
3489
+ var CheckProjectMeetingChecklistItemRequestSchema = z12.object({
3490
+ projectId: z12.string().cuid(),
3491
+ meetingId: z12.string().cuid(),
3492
+ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3493
+ checked: z12.boolean(),
3336
3494
  /** Card id or slug. Resolved server-side and required to be in the project. */
3337
- linkedTask: z10.string().min(1).optional(),
3338
- requestingUserId: z10.string().optional()
3339
- });
3340
- var EditProjectMeetingChecklistItemRequestSchema = z10.object({
3341
- projectId: z10.string().cuid(),
3342
- meetingId: z10.string().cuid(),
3343
- title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3344
- newTitle: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3345
- requestingUserId: z10.string().optional()
3346
- });
3347
- var RemoveProjectMeetingChecklistItemRequestSchema = z10.object({
3348
- projectId: z10.string().cuid(),
3349
- meetingId: z10.string().cuid(),
3350
- title: z10.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3351
- requestingUserId: z10.string().optional()
3352
- });
3353
- var CreateProjectMeetingRequestSchema = z10.object({
3354
- projectId: z10.string().cuid(),
3355
- rawText: z10.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
3356
- title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
3495
+ linkedTask: z12.string().min(1).optional(),
3496
+ requestingUserId: z12.string().optional()
3497
+ });
3498
+ var EditProjectMeetingChecklistItemRequestSchema = z12.object({
3499
+ projectId: z12.string().cuid(),
3500
+ meetingId: z12.string().cuid(),
3501
+ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3502
+ newTitle: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3503
+ requestingUserId: z12.string().optional()
3504
+ });
3505
+ var RemoveProjectMeetingChecklistItemRequestSchema = z12.object({
3506
+ projectId: z12.string().cuid(),
3507
+ meetingId: z12.string().cuid(),
3508
+ title: z12.string().min(1).max(MEETING_CHECKLIST_TITLE_MAX),
3509
+ requestingUserId: z12.string().optional()
3510
+ });
3511
+ var CreateProjectMeetingRequestSchema = z12.object({
3512
+ projectId: z12.string().cuid(),
3513
+ rawText: z12.string().min(1).max(MEETING_TRANSCRIPT_MAX_CHARS),
3514
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
3357
3515
  occurredAt: MeetingOccurredAtSchema.optional(),
3358
- requestingUserId: z10.string().optional()
3516
+ requestingUserId: z12.string().optional()
3359
3517
  });
3360
- var UpdateProjectMeetingRequestSchema = z10.object({
3361
- projectId: z10.string().cuid(),
3362
- meetingId: z10.string().cuid(),
3363
- title: z10.string().min(1).max(MEETING_TITLE_MAX).optional(),
3518
+ var UpdateProjectMeetingRequestSchema = z12.object({
3519
+ projectId: z12.string().cuid(),
3520
+ meetingId: z12.string().cuid(),
3521
+ title: z12.string().min(1).max(MEETING_TITLE_MAX).optional(),
3364
3522
  occurredAt: MeetingOccurredAtSchema.optional(),
3365
- summary: z10.string().min(1).max(MEETING_SUMMARY_MAX_CHARS).optional(),
3366
- requestingUserId: z10.string().optional()
3523
+ summary: z12.string().min(1).max(MEETING_SUMMARY_MAX_CHARS).optional(),
3524
+ requestingUserId: z12.string().optional()
3367
3525
  }).refine(
3368
3526
  (v) => v.title !== void 0 || v.occurredAt !== void 0 || v.summary !== void 0,
3369
3527
  "Pass at least one of title, occurredAt, or summary."
@@ -3738,13 +3896,17 @@ var CARD_TYPE_SURFACE = {
3738
3896
  task: "board",
3739
3897
  chat: "board",
3740
3898
  incident: "report",
3741
- suggestion: "report"
3899
+ suggestion: "report",
3900
+ // Decisions get their own surface: they are neither built on the board nor
3901
+ // triaged in the report feeds. Cards → Decisions is their only feed.
3902
+ decision: "decision"
3742
3903
  };
3743
3904
  var surfaceTypes = (surface) => Object.keys(CARD_TYPE_SURFACE).filter(
3744
3905
  (type) => CARD_TYPE_SURFACE[type] === surface
3745
3906
  );
3746
3907
  var BOARD_CARD_TYPES = surfaceTypes("board");
3747
3908
  var REPORT_CARD_TYPES = surfaceTypes("report");
3909
+ var DECISION_CARD_TYPES = surfaceTypes("decision");
3748
3910
 
3749
3911
  // src/runner/git-run.ts
3750
3912
  import { execFile } from "child_process";
@@ -8,7 +8,7 @@ import {
8
8
  readWorkspaceBytes,
9
9
  statWorkspacePath,
10
10
  workspacePathExists
11
- } from "./chunk-4ZDIBDQC.js";
11
+ } from "./chunk-I7QYTA2V.js";
12
12
  import {
13
13
  reportBootMilestone
14
14
  } from "./chunk-Q4FQOJ7D.js";
@@ -91,7 +91,7 @@ import {
91
91
  statWorkspacePath,
92
92
  updateRemoteToken,
93
93
  verifyGitCredential
94
- } from "./chunk-4ZDIBDQC.js";
94
+ } from "./chunk-I7QYTA2V.js";
95
95
  import {
96
96
  registerBootMilestoneSocketFallback,
97
97
  reportBootMilestone
@@ -6338,8 +6338,9 @@ Working rules:`,
6338
6338
  `- Read a file before your first Write/Edit to it, and batch multiple changes to the same file into a single call instead of many sequential edits.`,
6339
6339
  `- To learn what calls a symbol or where it lives, query the prebuilt code graph before grepping: \`graphify query "<SymbolName>"\` from the repo root. Query a SYMBOL, never a sentence \u2014 \`graphify query "resolveTaskBaseBranch"\` returns the definition plus every call site, while "how does a task get its base branch" seeds unrelated start nodes and returns test files and loggers. Don't know the symbol yet? Grep for the name first, then query it: grep finds names, the graph finds relationships. \`No matching nodes found\` means "not in this graph" (it is prebuilt, so very recent code is absent), NOT "not in the codebase" \u2014 fall back to \`git grep\`. Skip all of this if \`graphify-out/graph.json\` is not present.`,
6340
6340
  `- When a build/lint/test run fails, capture its output to a file once and grep the file \u2014 never re-run the suite just to re-filter the same output.`,
6341
- runtimeTui === "codex" ? `- Waiting on long-running commands: retain and resume the command session until it returns an exit result. Finish each required gate before ending the task. Never pretend a completion notification will resume you, and never start another gate while the current one is running.` : `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and the workspace stays awake for as long as background work is outstanding, so a backgrounded gate will not be killed by an idle sleep. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth \u2014 it survives a pod resume, which a background job does not. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
6342
- runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- Ending your turn with NO tool call is the correct way to wait, and it is safe: the pod stays alive and the next notification re-invokes you. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn.${agentMode === "review" ? "" : " For CI on a pushed commit the shape is `wait_for_checks`, then end the turn (the pod may sleep; the GitHub result wakes it)."} Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 a deploy, a remote queue \u2014 never for CI, and never as insurance against a background job's own notification, which does fire.`,
6341
+ `- App servers (including development and production servers) keep running after startup. Start them with the harness background/session facility, then run a separate bounded HTTP readiness check and continue verification. Never wait for server exit or a completion notification to mean the app is ready. If readiness times out, inspect build output and process/resource state before restarting.`,
6342
+ runtimeTui === "codex" ? `- Waiting on long-running commands: retain and resume the command session until it returns an exit result. Finish each required gate before ending the task. Never pretend a completion notification will resume you, and never start another gate while the current one is running.` : `- Waiting on long-running commands: if a gate finishes in under ~2 minutes, run it in the foreground with a timeout. For a longer one, launch it with run_in_background and STOP; a completion notification arrives when it finishes, and tracked background work holds the workspace awake for up to 45 minutes. Inspect any gate approaching that cap. For the final pre-PR gate a bounded foreground run (\`timeout 590 <gate>\` with Bash \`timeout: 600000\`) is still preferred as defense in depth. A pod restart loses both foreground and background processes. Never busy-wait with sleep/pgrep/tail loops, and never re-run the suite to escape a wait that looks stalled.`,
6343
+ runtimeTui === "codex" ? `- Do not end your turn to wait on a required gate. Resume its command session, inspect its exit result, and then continue the checklist.` : `- For a finite background gate, end your turn and its completion notification re-invokes you within the bounded liveness window. Never emit filler commands (\`echo waiting\`, \`true\`, \`sleep N; echo done\`) to "stay alive" \u2014 they are detected and blocked. The proven long-wait shape: start the job with run_in_background, then end the turn.${agentMode === "review" ? "" : " For CI on a pushed commit the shape is `wait_for_checks`, then end the turn (the pod may sleep; the GitHub result wakes it)."} Arm a ScheduleWakeup (delaySeconds 900-1500, prompt restating your next steps) only when nothing will notify you \u2014 a deploy, a remote queue \u2014 never for CI, and never as insurance against a background job's own notification, which does fire.`,
6343
6344
  `
6344
6345
  Git:`,
6345
6346
  `- Stay on \`${context.githubBranch}\` for the whole task: do not check out another branch and do not create one. It was cut from \`${context.baseBranch}\`, and PRs target that automatically.`,
@@ -6859,7 +6860,7 @@ var SEARCH_STATUSES = [
6859
6860
  "Complete",
6860
6861
  "Cancelled"
6861
6862
  ];
6862
- var SEARCH_CARD_TYPES = ["task", "incident", "suggestion"];
6863
+ var SEARCH_CARD_TYPES = ["task", "incident", "suggestion", "decision"];
6863
6864
  var searchFilterFields = {
6864
6865
  tagNames: f.optional(
6865
6866
  f.array(f.string(), {
@@ -7552,6 +7553,235 @@ var createSuggestionContract = defineToolContract({
7552
7553
  }
7553
7554
  });
7554
7555
  var suggestionsContracts = [createSuggestionContract];
7556
+ var OPTION_KINDS = ["current", "planned", "proposed"];
7557
+ var STATUS_LABELS = ["Open", "Decided", "Cancelled"];
7558
+ var optionDescription = "What this option means in the product, written as GIVEN / WHEN / THEN. Concrete enough that a reader six months from now can tell which behaviour was chosen.";
7559
+ var optionsDesc = "Two or three options. More than three means the question is not framed yet \u2014 narrow it or split it. Mark the option describing what the code does today as kind: current.";
7560
+ var defaultOptionDesc = "1-based position of the option that wins if nobody decides by the deadline. Required: silence has to become a decision, never a stall.";
7561
+ var agentOption = f.object(
7562
+ {
7563
+ label: f.string({ min: 1, desc: "Short name for the option, under 80 characters" }),
7564
+ description: f.string({ min: 1, desc: optionDescription }),
7565
+ kind: f.optional(
7566
+ f.enum(OPTION_KINDS, {
7567
+ desc: 'current = what the code does today, planned = already agreed but unbuilt, proposed = new. Defaults to "proposed".'
7568
+ })
7569
+ )
7570
+ },
7571
+ { desc: "One option people can choose" }
7572
+ );
7573
+ var createDecisionContract = defineToolContract({
7574
+ name: "create_decision",
7575
+ agent: {
7576
+ description: `Raise a product decision: a fork the plan cannot be right without. Two or three options, one default, a deadline. Run list_decisions FIRST \u2014 an answered decision is the answer, and re-asking it wastes everybody's time. Do NOT file a decision for an assumption you can simply state: an assumed behaviour belongs in the plan as a GIVEN / WHEN / THEN line under "Assumed behaviours". Returns the decision's id, slug, and url.`,
7577
+ fields: {
7578
+ question: f.string({
7579
+ min: 1,
7580
+ desc: "The question, as one plain sentence ending in a question mark"
7581
+ }),
7582
+ summary: f.optional(
7583
+ f.string({
7584
+ desc: cardDescriptionDesc("One or two sentences of context for the card face")
7585
+ })
7586
+ ),
7587
+ context: f.string({
7588
+ min: 1,
7589
+ desc: "Markdown background: why this is being asked now, and what breaks if it is answered wrongly"
7590
+ }),
7591
+ options: f.array(agentOption, { min: 2, desc: optionsDesc }),
7592
+ default_option: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
7593
+ deadline_hours: f.optional(
7594
+ f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
7595
+ ),
7596
+ impacted_cards: f.optional(
7597
+ f.array(f.string(), {
7598
+ desc: "Slugs or ids of cards this decision blocks. They cannot start until it is settled, and they are un-blocked the moment it is. Blocking a card needs Moderate access: without it the decision is still raised and each ref comes back under refused."
7599
+ })
7600
+ ),
7601
+ include_current_card: f.optional(
7602
+ f.boolean({
7603
+ desc: "Also block the card this session is working on. Defaults to true \u2014 you are raising this because your own plan is stuck on it. Pass false to raise the question without stopping your own work."
7604
+ })
7605
+ ),
7606
+ tag_names: f.optional(
7607
+ f.array(f.string(), {
7608
+ desc: "Tag names. The settled decision is written into each tag's overview, which is how the next agent finds it."
7609
+ })
7610
+ ),
7611
+ raised_by_pr: f.optional(
7612
+ f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
7613
+ )
7614
+ }
7615
+ },
7616
+ mcp: {
7617
+ description: "Raise a product decision on a Conveyor project: a fork that has to be settled before the work can be planned correctly. Two or three options, one default that applies if nobody decides, and a deadline. Search list_decisions first \u2014 a Decided decision is the answer. Pass projectId to target a specific project; otherwise the configured default project is used.",
7618
+ fields: {
7619
+ projectId: mcpProjectId,
7620
+ question: f.string({ desc: "The question, as one plain sentence" }),
7621
+ summary: f.optional(f.string({ desc: cardDescriptionDesc("Context for the card face") })),
7622
+ context: f.string({ desc: "Markdown background: why this is being asked now" }),
7623
+ options: f.array(agentOption, { min: 2, desc: optionsDesc }),
7624
+ defaultOption: f.number({ int: true, min: 1, max: 3, desc: defaultOptionDesc }),
7625
+ deadlineHours: f.optional(
7626
+ f.number({ min: 1, max: 720, desc: "Hours until the default applies. Default 72." })
7627
+ ),
7628
+ impactedCards: f.optional(
7629
+ f.array(f.string(), {
7630
+ desc: "Slugs or ids of cards this decision blocks. Needs Moderate access; without it each ref comes back under refused and the decision is still raised."
7631
+ })
7632
+ ),
7633
+ tagNames: f.optional(f.array(f.string(), { desc: "Tag names to categorize the decision" })),
7634
+ raisedByPr: f.optional(
7635
+ f.number({ int: true, min: 1, desc: "PR number that surfaced the question" })
7636
+ )
7637
+ }
7638
+ }
7639
+ });
7640
+ var listDecisionsContract = defineToolContract({
7641
+ name: "list_decisions",
7642
+ agent: {
7643
+ description: `Search this project's decisions. Run this BEFORE planning anything in an area: "have we already decided how X behaves?" Decided rows carry their resolution, so the list doubles as the decision log. Filter by card to see what is blocking a specific card.`,
7644
+ fields: {
7645
+ status: f.optional(
7646
+ f.enum(STATUS_LABELS, {
7647
+ desc: "Open = still being voted on, Decided = settled, Cancelled = dropped"
7648
+ })
7649
+ ),
7650
+ tag: f.optional(f.string({ desc: 'Tag name, e.g. "slack"' })),
7651
+ card: f.optional(
7652
+ f.string({ desc: "Slug or id of a card \u2014 returns the decisions it is blocked by" })
7653
+ ),
7654
+ query: f.optional(f.string({ desc: "Text search on the question" })),
7655
+ limit: f.optional(f.number({ desc: "Max rows to return (default 50, max 200)" }))
7656
+ }
7657
+ },
7658
+ mcp: {
7659
+ description: `List and search a Conveyor project's decisions. Decided rows include their resolution, so this is the fastest way to answer "did we already settle this?". Pass projectId to target a specific project; otherwise the configured default project is used.`,
7660
+ fields: {
7661
+ projectId: mcpProjectId,
7662
+ status: f.optional(f.enum(STATUS_LABELS, { desc: "Filter by state" })),
7663
+ tag: f.optional(f.string({ desc: "Tag name to filter by" })),
7664
+ card: f.optional(f.string({ desc: "Card slug or id \u2014 decisions blocking that card" })),
7665
+ query: f.optional(f.string({ desc: "Text search on the question" })),
7666
+ limit: f.optional(
7667
+ f.number({ int: true, min: 1, max: 200, desc: "Max rows to return (default 50)" })
7668
+ )
7669
+ }
7670
+ }
7671
+ });
7672
+ var getDecisionContract = defineToolContract({
7673
+ name: "get_decision",
7674
+ agent: {
7675
+ description: "Read one decision in full: its context, every option with its GIVEN / WHEN / THEN, every vote with who cast it and why, the running tally, the cards it blocks, and the resolution once there is one. Use read_task_chat with the same id for the discussion thread.",
7676
+ fields: {
7677
+ decision: f.string({ min: 1, desc: "Decision slug or id" })
7678
+ }
7679
+ },
7680
+ mcp: {
7681
+ description: "Read one Conveyor decision in full \u2014 context, options, votes with reasons, tally, impacted cards, and the resolution. Pass projectId to disambiguate a slug; otherwise the configured default project is used.",
7682
+ fields: {
7683
+ projectId: mcpProjectId,
7684
+ decision: f.string({ desc: "Decision slug or id" })
7685
+ }
7686
+ }
7687
+ });
7688
+ var voteDecisionContract = defineToolContract({
7689
+ name: "vote_decision",
7690
+ agent: {
7691
+ description: "Vote on an open decision. One vote per voter: voting again replaces your previous one. Always give a reason \u2014 the resolution weighs reasons over counts, and a vote with no reason carries almost nothing.",
7692
+ fields: {
7693
+ decision: f.string({ min: 1, desc: "Decision slug or id" }),
7694
+ option: f.string({ min: 1, desc: "Option id, or its 1-based position" }),
7695
+ reason: f.optional(
7696
+ f.string({ desc: "Why this option, in one or two sentences (max 255 characters)" })
7697
+ )
7698
+ }
7699
+ },
7700
+ mcp: {
7701
+ description: "Vote on an open Conveyor decision, with a reason. One vote per voter \u2014 voting again replaces the previous one.",
7702
+ fields: {
7703
+ projectId: mcpProjectId,
7704
+ decision: f.string({ desc: "Decision slug or id" }),
7705
+ option: f.string({ desc: "Option id, or its 1-based position" }),
7706
+ reason: f.optional(f.string({ desc: "Why this option (max 255 characters)" }))
7707
+ }
7708
+ }
7709
+ });
7710
+ var resolveDecisionContract = defineToolContract({
7711
+ name: "resolve_decision",
7712
+ agent: {
7713
+ description: "Settle a decision. This is not a formality: the resolution is written into the plan of every card the decision blocked, into each of their chats, and into the overview of every tag it carries, and those cards become startable. Say what was decided in one or two sentences \u2014 a reader months later should not need to open the thread.",
7714
+ fields: {
7715
+ decision: f.string({ min: 1, desc: "Decision slug or id" }),
7716
+ option: f.string({ min: 1, desc: "Winning option id, or its 1-based position" }),
7717
+ resolution: f.string({
7718
+ min: 1,
7719
+ desc: "What was decided, in one or two plain sentences (max 255 characters)"
7720
+ }),
7721
+ notes: f.optional(
7722
+ f.string({ desc: "Markdown reasoning. Link any verdict attachment here." })
7723
+ )
7724
+ }
7725
+ },
7726
+ mcp: {
7727
+ description: "Settle a Conveyor decision. The resolution is written into every impacted card's plan and chat and into its tags' overviews, and the impacted cards become startable.",
7728
+ fields: {
7729
+ projectId: mcpProjectId,
7730
+ decision: f.string({ desc: "Decision slug or id" }),
7731
+ option: f.string({ desc: "Winning option id, or its 1-based position" }),
7732
+ resolution: f.string({ desc: "What was decided (max 255 characters)" }),
7733
+ notes: f.optional(f.string({ desc: "Markdown reasoning" }))
7734
+ }
7735
+ }
7736
+ });
7737
+ var updateDecisionContract = defineToolContract({
7738
+ name: "update_decision",
7739
+ agent: {
7740
+ description: "Change an open decision: extend its deadline, link or unlink the cards it blocks, or cancel it. Cancelling un-blocks the impacted cards without answering the question, so only do it when the question stopped mattering.",
7741
+ fields: {
7742
+ decision: f.string({ min: 1, desc: "Decision slug or id" }),
7743
+ deadline_hours: f.optional(f.number({ desc: "New deadline, in hours from now. 1 to 720." })),
7744
+ add_impacted_cards: f.optional(
7745
+ f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
7746
+ ),
7747
+ remove_impacted_cards: f.optional(
7748
+ f.array(f.string(), { desc: "Ids of cards to stop blocking" })
7749
+ ),
7750
+ status: f.optional(
7751
+ f.enum(["Cancelled", "Open"], {
7752
+ desc: "Cancelled drops the question; Open reopens a settled one and clears its answer"
7753
+ })
7754
+ )
7755
+ }
7756
+ },
7757
+ mcp: {
7758
+ description: "Change an open Conveyor decision: extend the deadline, link or unlink impacted cards, cancel it, or reopen a settled one.",
7759
+ fields: {
7760
+ projectId: mcpProjectId,
7761
+ decision: f.string({ desc: "Decision slug or id" }),
7762
+ deadlineHours: f.optional(
7763
+ f.number({ min: 1, max: 720, desc: "New deadline, in hours from now" })
7764
+ ),
7765
+ addImpactedCards: f.optional(
7766
+ f.array(f.string(), { desc: "Slugs or ids of cards to block on this decision" })
7767
+ ),
7768
+ removeImpactedCards: f.optional(
7769
+ f.array(f.string(), { desc: "Ids of cards to stop blocking" })
7770
+ ),
7771
+ status: f.optional(
7772
+ f.enum(["Cancelled", "Open"], { desc: "Cancel it, or reopen a settled one" })
7773
+ )
7774
+ }
7775
+ }
7776
+ });
7777
+ var decisionsContracts = [
7778
+ createDecisionContract,
7779
+ listDecisionsContract,
7780
+ getDecisionContract,
7781
+ voteDecisionContract,
7782
+ resolveDecisionContract,
7783
+ updateDecisionContract
7784
+ ];
7555
7785
  var createPullRequestContract = defineToolContract({
7556
7786
  name: "create_pull_request",
7557
7787
  agent: {
@@ -8212,6 +8442,7 @@ var TOOL_CONTRACTS = Object.fromEntries(
8212
8442
  ...subtasksContracts,
8213
8443
  ...attachmentsContracts,
8214
8444
  ...suggestionsContracts,
8445
+ ...decisionsContracts,
8215
8446
  ...pullRequestContracts,
8216
8447
  ...integrationsContracts,
8217
8448
  ...driveContracts,
@@ -10464,6 +10695,143 @@ ${res.map(describeItem).join("\n")}`);
10464
10695
  ];
10465
10696
  }
10466
10697
 
10698
+ // src/tools/decision-tools.ts
10699
+ function errText5(prefix, error) {
10700
+ return textResult(`${prefix}: ${error instanceof Error ? error.message : "Unknown error"}`);
10701
+ }
10702
+ function buildDecisionTools(connection, projectId) {
10703
+ return [
10704
+ ...readTools2(connection, projectId),
10705
+ ...raiseAndVoteTools(connection, projectId),
10706
+ ...settleTools(connection, projectId)
10707
+ ];
10708
+ }
10709
+ function readTools2(connection, projectId) {
10710
+ return [
10711
+ defineContractTool(
10712
+ listDecisionsContract,
10713
+ async (input) => {
10714
+ try {
10715
+ const res = await connection.call("listProjectDecisions", {
10716
+ projectId,
10717
+ status: input.status,
10718
+ tag: input.tag,
10719
+ card: input.card,
10720
+ query: input.query,
10721
+ limit: input.limit
10722
+ });
10723
+ if (res.decisions.length === 0) {
10724
+ return textResult(
10725
+ "No decisions match. Nothing has been settled here, so you are free to choose \u2014 state the choice in the plan as a GIVEN / WHEN / THEN assumption, or raise a decision with create_decision if the plan cannot be right without an answer."
10726
+ );
10727
+ }
10728
+ return textResult(JSON.stringify(res.decisions, null, 2));
10729
+ } catch (error) {
10730
+ return errText5("Failed to list decisions", error);
10731
+ }
10732
+ },
10733
+ { annotations: { readOnlyHint: true } }
10734
+ ),
10735
+ defineContractTool(
10736
+ getDecisionContract,
10737
+ async (input) => {
10738
+ try {
10739
+ const res = await connection.call("getProjectDecision", {
10740
+ projectId,
10741
+ decision: input.decision
10742
+ });
10743
+ return textResult(JSON.stringify(res, null, 2));
10744
+ } catch (error) {
10745
+ return errText5("Failed to read the decision", error);
10746
+ }
10747
+ },
10748
+ { annotations: { readOnlyHint: true } }
10749
+ )
10750
+ ];
10751
+ }
10752
+ function raiseAndVoteTools(connection, projectId) {
10753
+ return [
10754
+ defineContractTool(createDecisionContract, async (input) => {
10755
+ try {
10756
+ const res = await connection.call("createProjectDecision", {
10757
+ projectId,
10758
+ question: input.question,
10759
+ summary: input.summary,
10760
+ context: input.context,
10761
+ options: input.options,
10762
+ defaultOption: input.default_option,
10763
+ deadlineHours: input.deadline_hours,
10764
+ impactedCards: input.impacted_cards,
10765
+ includeCurrentCard: input.include_current_card,
10766
+ sessionId: connection.sessionId,
10767
+ tagNames: input.tag_names,
10768
+ raisedByPr: input.raised_by_pr
10769
+ });
10770
+ const refused = res.refused.length > 0 ? `
10771
+
10772
+ Not linked: ${res.refused.map((r) => `${r.ref} (${r.reason})`).join("; ")}` : "";
10773
+ return textResult(
10774
+ `Raised decision ${res.slug} \u2014 ${res.url}
10775
+ Blocking ${res.impactedLinked.length} card(s).${refused}`
10776
+ );
10777
+ } catch (error) {
10778
+ return errText5("Failed to raise the decision", error);
10779
+ }
10780
+ }),
10781
+ defineContractTool(voteDecisionContract, async (input) => {
10782
+ try {
10783
+ const res = await connection.call("voteProjectDecision", {
10784
+ projectId,
10785
+ decision: input.decision,
10786
+ option: input.option,
10787
+ reason: input.reason
10788
+ });
10789
+ const tally = res.options.map((o) => `${o.label}: ${o.votes}`).join(", ");
10790
+ return textResult(`Voted on ${res.slug}. Tally \u2014 ${tally}.`);
10791
+ } catch (error) {
10792
+ return errText5("Failed to vote", error);
10793
+ }
10794
+ })
10795
+ ];
10796
+ }
10797
+ function settleTools(connection, projectId) {
10798
+ return [
10799
+ defineContractTool(resolveDecisionContract, async (input) => {
10800
+ try {
10801
+ const res = await connection.call("resolveProjectDecision", {
10802
+ projectId,
10803
+ decision: input.decision,
10804
+ option: input.option,
10805
+ resolution: input.resolution,
10806
+ notes: input.notes
10807
+ });
10808
+ return textResult(
10809
+ `Settled ${res.slug} as "${res.chosenOption}". ${res.impactedCount} card(s) were written back to and are no longer blocked.`
10810
+ );
10811
+ } catch (error) {
10812
+ return errText5("Failed to settle the decision", error);
10813
+ }
10814
+ }),
10815
+ defineContractTool(updateDecisionContract, async (input) => {
10816
+ try {
10817
+ const res = await connection.call("updateProjectDecision", {
10818
+ projectId,
10819
+ decision: input.decision,
10820
+ deadlineHours: input.deadline_hours,
10821
+ addImpactedCards: input.add_impacted_cards,
10822
+ removeImpactedCards: input.remove_impacted_cards,
10823
+ status: input.status
10824
+ });
10825
+ return textResult(
10826
+ `Updated ${res.slug} \u2014 ${res.status}, ${res.impactedCount} impacted card(s), deadline ${res.deadlineAt}.`
10827
+ );
10828
+ } catch (error) {
10829
+ return errText5("Failed to update the decision", error);
10830
+ }
10831
+ })
10832
+ ];
10833
+ }
10834
+
10467
10835
  // src/tools/connected-tools.ts
10468
10836
  function connectedToolsFor(connection, context) {
10469
10837
  const projectId = context?.projectId;
@@ -10473,6 +10841,9 @@ function connectedToolsFor(connection, context) {
10473
10841
  buildListProjectIntegrationsTool(connection, projectId),
10474
10842
  ...context.chatChannelsConfigured ? buildChannelTools(connection, projectId) : [],
10475
10843
  ...context.meetingsAvailable ? buildMeetingTools(connection, projectId) : [],
10844
+ // Always available: the decision tables exist on every project, and a fork
10845
+ // an agent cannot file is a fork it assumes its way past instead.
10846
+ ...buildDecisionTools(connection, projectId),
10476
10847
  ...context.googleAnalyticsConfigured ? [buildAnalyticsSummaryTool(connection, projectId)] : [],
10477
10848
  ...context.gcpLogsConfigured ? [buildQueryGcpLogsTool(connection, projectId)] : [],
10478
10849
  ...context.grafanaLogsConfigured ? [buildQueryGrafanaLogsTool(connection, projectId)] : []
@@ -11482,6 +11853,8 @@ function matchesReadOnlyBlocked(cmd) {
11482
11853
  }
11483
11854
  var POST_CHANNEL_TOOL = /(^|__)post_channel_message$/;
11484
11855
  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.";
11856
+ var DECISION_SETTLE_TOOLS = /(^|__)(resolve_decision|update_decision)$/;
11857
+ var DECISION_SETTLE_DENIED = "Settling a decision is not available in review mode. Say on the PR that the change turns on an unsettled decision, and cite it with list_decisions \u2014 a reviewer does not make the product call.";
11485
11858
  function handleReadOnlyToolAccess(toolName, input) {
11486
11859
  if (POST_CHANNEL_TOOL.test(toolName)) {
11487
11860
  return { behavior: "deny", message: CHANNEL_POST_DENIED };
@@ -11531,6 +11904,9 @@ function handleReviewToolAccess(toolName, input) {
11531
11904
  if (POST_CHANNEL_TOOL.test(toolName)) {
11532
11905
  return { behavior: "deny", message: CHANNEL_POST_DENIED };
11533
11906
  }
11907
+ if (DECISION_SETTLE_TOOLS.test(toolName)) {
11908
+ return { behavior: "deny", message: DECISION_SETTLE_DENIED };
11909
+ }
11534
11910
  return handleBuildingToolAccess(toolName, input);
11535
11911
  }
11536
11912
  function handleAutoToolAccess(toolName, input, hasExitedPlanMode, isParentTask) {
@@ -14075,8 +14451,8 @@ var SessionRunner = class _SessionRunner {
14075
14451
  "[conveyor-agent] Completed \u2014 entering dormant idle (staying connected)\n"
14076
14452
  );
14077
14453
  }
14454
+ this.pendingMessages = this.pendingMessages.filter((msg) => msg.source === "pty_passive");
14078
14455
  await this.flushWipNow("WIP: turn complete");
14079
- this.pendingMessages.length = 0;
14080
14456
  if (this._state !== "idle") await this.setState("idle");
14081
14457
  const remainingMs = Math.max(0, this.dormantDeadline - Date.now());
14082
14458
  this.lifecycle.startDormantTimer(remainingMs);
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  WorkspaceCommandSupervisor,
8
8
  startWorkspaceCommandsAfterConnect,
9
9
  stopWorkspaceCommands
10
- } from "./chunk-5WQHDGIW.js";
10
+ } from "./chunk-LBB7DXOC.js";
11
11
  import {
12
12
  PtyHarness,
13
13
  SessionRunner,
@@ -25,7 +25,7 @@ import {
25
25
  runUsageProbe,
26
26
  sampleKeyUsage,
27
27
  usesNativeUsageReporting
28
- } from "./chunk-WPXSZKMS.js";
28
+ } from "./chunk-XJSW42TM.js";
29
29
  import "./chunk-37J5MMQT.js";
30
30
  import {
31
31
  inheritedEnv,
@@ -44,7 +44,7 @@ import {
44
44
  createServiceLogger,
45
45
  fetchBootstrap,
46
46
  loadConveyorConfig
47
- } from "./chunk-4ZDIBDQC.js";
47
+ } from "./chunk-I7QYTA2V.js";
48
48
  import "./chunk-Q4FQOJ7D.js";
49
49
  import "./chunk-W4LZ7R6Z.js";
50
50
  import "./chunk-372R6E4C.js";
@@ -1205,7 +1205,7 @@ function wireSpawnChildren(mode, connection, supervisors, logger7) {
1205
1205
 
1206
1206
  // src/cli.ts
1207
1207
  if (process.argv[2] === "boot") {
1208
- const { runBoot } = await import("./boot-Z3EBJ7GX.js");
1208
+ const { runBoot } = await import("./boot-S73ZNWBJ.js");
1209
1209
  process.exit(await runBoot(process.argv.slice(3)));
1210
1210
  }
1211
1211
  if (isLegacyEntrypointLaunch(process.env)) {
@@ -1458,7 +1458,7 @@ if (!RUNNER_MODES.includes(CONVEYOR_MODE)) {
1458
1458
  process.exit(1);
1459
1459
  }
1460
1460
  if (CONVEYOR_MODE === "serving") {
1461
- const { runServingSession } = await import("./serve-boot-IRRHYA7W.js");
1461
+ const { runServingSession } = await import("./serve-boot-ODH57W6I.js");
1462
1462
  exitContext.runnerMode = "serving";
1463
1463
  exitContext.sessionId = process.env.CONVEYOR_SESSION_ID ?? exitContext.sessionId;
1464
1464
  const outcome = await runServingSession({
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SessionRunner,
3
3
  unshallowRepo
4
- } from "./chunk-WPXSZKMS.js";
4
+ } from "./chunk-XJSW42TM.js";
5
5
  import "./chunk-37J5MMQT.js";
6
6
  import "./chunk-W5INK3NE.js";
7
7
  import {
@@ -17,7 +17,7 @@ import {
17
17
  stageAndCommit,
18
18
  updateRemoteToken,
19
19
  workspacePathExists
20
- } from "./chunk-4ZDIBDQC.js";
20
+ } from "./chunk-I7QYTA2V.js";
21
21
  import "./chunk-Q4FQOJ7D.js";
22
22
  import {
23
23
  runAuthTokenCommand,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  WorkspaceCommandSupervisor
3
- } from "./chunk-5WQHDGIW.js";
3
+ } from "./chunk-LBB7DXOC.js";
4
4
  import {
5
5
  AgentConnection,
6
6
  CodespacePortVisibility,
@@ -11,7 +11,7 @@ import {
11
11
  createServiceLogger,
12
12
  ensureOnTaskBranch,
13
13
  loadConveyorConfig
14
- } from "./chunk-4ZDIBDQC.js";
14
+ } from "./chunk-I7QYTA2V.js";
15
15
  import "./chunk-Q4FQOJ7D.js";
16
16
  import "./chunk-W4LZ7R6Z.js";
17
17
  import "./chunk-IA45XHOA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rallycry/conveyor-agent",
3
- "version": "11.0.21",
3
+ "version": "11.0.23",
4
4
  "description": "Conveyor Agent Runner v10 - PTY harness for the task chat (SDK harness for audit/project-chat). Agent-as-User architecture with BaseService patterns. Works locally too.",
5
5
  "keywords": [
6
6
  "agent",
@@ -215,6 +215,27 @@ behavior — do not go looking for why "unrelated" tests are running.
215
215
  > more broadly; if it does, let CI finish before merging. Confirm which case
216
216
  > you are in rather than assuming, since the two lead to opposite behavior.
217
217
 
218
+ ## A plan that turns on an unsettled question
219
+
220
+ A plan you cannot execute correctly because a product choice was never made is
221
+ not a blocker by itself. Check `list_decisions(card: "<slug>")` and the owning
222
+ tag's overview first — the answer is often already there, and citing it is the
223
+ whole point of the type. When it genuinely is not:
224
+
225
+ - **Most of the time, decide and say so.** Write the choice into the plan as a
226
+ GIVEN / WHEN / THEN line under "Assumed behaviours" and keep building. A
227
+ reviewer who disagrees says so, and it costs one comment.
228
+ - **Raise a decision when the answer changes what you build** and getting it
229
+ wrong means rework rather than a follow-up commit. `create_decision` blocks
230
+ this card by default, so proceed on the decision's default option and mark
231
+ the plan `Planned: <default> pending <decision url>` — the answer is written
232
+ back onto this card's plan when it settles.
233
+ - **Park only when building on the default would be thrown away.** That is the
234
+ Blocked path below.
235
+
236
+ The bar, and how to write options somebody can actually choose between, is in
237
+ [../conveyor-plan/references/product-decisions.md](../conveyor-plan/references/product-decisions.md).
238
+
218
239
  ## Not every task ends in a PR
219
240
 
220
241
  `create_pull_request` is for work that changes code. Plenty of cards don't:
@@ -30,7 +30,8 @@ Announce a one-line plan naming the sources you found, then sweep. **Say what yo
30
30
  | Source | How |
31
31
  | ------ | --- |
32
32
  | Work channels | `read_channel_messages` per registered readable channel, paging back with `olderCursor` until the window covers the question. `authorIsBot` separates the team's discussion from Conveyor's own card feed — check it before treating a message as a teammate's. Read thread replies (`threadTs`) where a thread carries the argument. |
33
- | Conveyor cards | `search_tasks` with ALL `typeFilters` (task, incident, suggestion) and several keyword variants — the term, the term plus symptom words, the adjacent nouns people actually use. Incidents carry fingerprint dedup, so an incident's upvote count is itself a frequency signal. Check whether a decision card already exists; the doc is usually its input. |
33
+ | Conveyor cards | `search_tasks` with ALL `typeFilters` (task, incident, suggestion) and several keyword variants — the term, the term plus symptom words, the adjacent nouns people actually use. Incidents carry fingerprint dedup, so an incident's upvote count is itself a frequency signal. |
34
+ | Decisions | `list_decisions` — open ones are the questions on the table, Decided ones carry their resolution and are the project's own record of what it already settled. A verdict that contradicts a Decided decision has to say so and say why. `list_project_integrations` reports `decisions.openCount` / `decidedCount`, so you know whether there is a log here before you search it. |
34
35
  | Tags | `list_tags` then `get_tag` on the relevant ones — the overview is the project's own domain vocabulary, and it names the subsystems your categories should line up with. |
35
36
  | Card chat | `read_task_chat` on the cards the search surfaced. The argument usually lives in the chat, not the description. |
36
37
  | Meetings | If the meeting tools exist, list and read the ones in the window. A transcript is the densest source of "what people actually said" you will find. |
@@ -83,6 +84,48 @@ Chart rules that repeatedly matter: plain div/CSS charts over JS; one strong cha
83
84
  4. **Local sessions may also publish an Artifact** for a shareable URL, and `SendUserFile` the HTML — both are additive. In a pod neither exists; the attachment is the deliverable.
84
85
  5. **Revisions re-upload to the SAME card.** The doc is living; never fork it into `report-v2.html`.
85
86
 
87
+ ## Decision mode
88
+
89
+ When the argument IS a decision card — the user named one, or the sweep found
90
+ an open decision that this question is about — the shape changes. The doc is
91
+ not a survey of opinion; it is the input to settling a specific question that
92
+ has options, a deadline, and a default that wins if nobody acts.
93
+
94
+ **Sources, in place of the Phase 1 table:**
95
+
96
+ | Source | How |
97
+ | ------ | --- |
98
+ | The decision | `get_decision` — the context, every option's GIVEN / WHEN / THEN, every vote with who cast it and why, the running tally, and the cards it blocks. |
99
+ | The thread | `read_task_chat` on the decision. The Slack and Discord replies already mirror here, so this is the whole discussion in one read. |
100
+ | The impacted cards | Their plans say what each option would cost to build. A clean option that nobody can implement is not the cheap one. |
101
+ | Everything else | The ordinary sweep above, scoped to the question. |
102
+
103
+ **Weigh reasons, not counts.** Two people giving the same reason are one
104
+ argument. A single vote with a concrete failure case outranks three with none.
105
+ Say so explicitly in the verdict — a decision settled on a head count that
106
+ contradicted the reasoning is the thing this mode exists to prevent.
107
+
108
+ **Then settle it, in this order:**
109
+
110
+ 1. Attach the verdict HTML. **Where it lands depends on the surface**, and the
111
+ difference matters: a local MCP session's `upload_attachment` takes a
112
+ `taskId`, so send it to the DECISION card, titled with the question. The
113
+ in-pod tool takes no card argument and always posts to the session's own
114
+ card — so in a pod, attach it there and put the link in the resolution
115
+ notes rather than pretending it reached the decision.
116
+ 2. `resolve_decision` with the winning option, a resolution of one or two
117
+ plain sentences (255 characters), and `notes` linking wherever the
118
+ attachment actually landed.
119
+
120
+ Step 2 is what makes it real: the resolution is written into the plan and the
121
+ chat of every card the decision blocked and into the owning tags' overviews,
122
+ and those cards become startable. Do not stop at the doc.
123
+
124
+ **If the evidence does not settle it**, say that and do not resolve. Extend the
125
+ deadline with `update_decision` and post what would settle it. A verdict of
126
+ "this needs one more data point, and here is which" is a real answer; picking
127
+ an option to look decisive is not.
128
+
86
129
  ## Phase 5: the consensus loop
87
130
 
88
131
  The doc is the midpoint, not the end. Expect and serve:
@@ -121,6 +121,17 @@ Ask the user only decisions that change the plan's shape — scope cuts, UX
121
121
  choices, irreversible tradeoffs. Batch them in one round; never drip. Facts
122
122
  the repo can answer are yours to find, not theirs.
123
123
 
124
+ **Search the decision log first, and raise one when the user is not there.**
125
+ `list_decisions(status: "Decided", tag: "<tag>")` often answers the question
126
+ outright — an answered decision is the answer, and re-asking it is the waste
127
+ this exists to stop. When it does not, and you are running headless, prefer
128
+ `create_decision` over blocking on `AskUserQuestion`: a decision has a
129
+ deadline, so it settles on its default rather than stalling, and it links the
130
+ cards it blocks. Most forks are not decisions at all — see
131
+ [references/product-decisions.md](references/product-decisions.md) for the bar,
132
+ how to write options a person can actually choose between, and how to keep
133
+ building while one is open.
134
+
124
135
  ## Phase 3 — Draft the plan
125
136
 
126
137
  Use the plan format in [references/plan-format.md](references/plan-format.md):
@@ -0,0 +1,103 @@
1
+ # Product decisions
2
+
3
+ A product decision is a fork the plan cannot be right without — where two
4
+ answers lead to two different, both-defensible builds, and picking one silently
5
+ means somebody finds out at review. Conveyor has a card type for it:
6
+ `create_decision` raises one with two or three options, a default, and a
7
+ deadline; `list_decisions` finds the answer next time.
8
+
9
+ Shared by the plan, triage, build, and review skills. The rules are the same
10
+ wherever you are.
11
+
12
+ ## 1. Search before you ask
13
+
14
+ A decision that was settled once must never be re-litigated. Before planning
15
+ anything in an area:
16
+
17
+ 1. Read the owning tag's overview (`get_tag`) — a settled decision is written
18
+ into a `## Decisions` section there.
19
+ 2. `list_decisions(status: "Decided", tag: "<tag>")` — every row carries its
20
+ resolution, so the list IS the decision log.
21
+ 3. `list_decisions(card: "<slug>")` — what is blocking this card right now.
22
+
23
+ Cite what you find. "We decided this on 2026-09-19: <resolution>" ends the
24
+ question; re-opening it without new information wastes everybody's time.
25
+
26
+ ## 2. Most forks are not decisions
27
+
28
+ The common case is an assumption you can simply state. Write it in the plan
29
+ under **Assumed behaviours**, one GIVEN / WHEN / THEN line each:
30
+
31
+ ```
32
+ ## Assumed behaviours
33
+ - GIVEN a decision with no votes, WHEN its deadline passes, THEN the default option wins.
34
+ ```
35
+
36
+ A reviewer who disagrees says so, and it costs one comment. That is cheaper
37
+ than a card, a deadline, and three people's attention.
38
+
39
+ Raise a decision only when **all** of these hold:
40
+
41
+ - Two or three options are genuinely defensible — you cannot pick on merit.
42
+ - Getting it wrong means rework, not a follow-up commit.
43
+ - Somebody other than you has to live with the answer.
44
+
45
+ ## 3. Writing one that is answerable
46
+
47
+ - **The question is one plain sentence**, ending in a question mark. If it
48
+ needs two, it is two decisions.
49
+ - **Two or three options.** More than three means the question is not framed
50
+ yet: narrow it, or split it.
51
+ - **Each option is GIVEN / WHEN / THEN** — what the product does, concretely,
52
+ if this option wins. "Option B: use a queue" is not an option; "GIVEN a
53
+ second build request, WHEN one is already running, THEN it queues rather
54
+ than being refused" is.
55
+ - **Mark the status quo.** The option describing what the code does today gets
56
+ `kind: "current"`; an already-agreed-but-unbuilt one gets `kind: "planned"`.
57
+ A reader six months later needs to know whether the winner was a change.
58
+ - **Name the default and what it costs if wrong.** The default wins on
59
+ silence. If silence would be dangerous, say so in the context — and set a
60
+ shorter deadline.
61
+ - **Link the cards it blocks.** They cannot start until it settles, and they
62
+ un-block the moment it does. In a pod this includes your own card by
63
+ default.
64
+ - **Tag it.** The settled answer is written into each tag's overview, which is
65
+ how the next agent finds it without knowing the decision exists.
66
+
67
+ ## 4. Keep moving
68
+
69
+ Raising a decision is not a reason to stop. Either:
70
+
71
+ - **Proceed on the default.** Mark the plan: `Planned: <default option>
72
+ pending <decision url>`. If the decision settles differently, the write-back
73
+ lands on this card's plan and the card is un-blocked with the correct answer.
74
+ - **Park**, per the parked protocol in
75
+ `conveyor-build/references/pack-path.md`, when building on the default would
76
+ be actively unsafe or would be thrown away.
77
+
78
+ Never sit idle waiting for a vote.
79
+
80
+ ## 5. Settling one
81
+
82
+ `resolve_decision` is not a formality. The resolution is written into the plan
83
+ and the chat of every card the decision blocked, and into the overview of every
84
+ tag it carries — and those cards become startable. So:
85
+
86
+ - Write the resolution as one or two plain sentences a non-engineer can read.
87
+ A reader months later should not have to open the thread.
88
+ - Weigh the reasons, not the count. Two people with the same reason are one
89
+ argument.
90
+ - Put the reasoning in `notes`, and link any verdict attachment there.
91
+
92
+ **A reviewer does not settle decisions.** A review runs against a diff that
93
+ already exists. If the diff turns on an unsettled decision, say so on the PR
94
+ and cite it — `resolve_decision` is refused in review mode for that reason.
95
+
96
+ ## 6. When nobody answers
97
+
98
+ At the deadline, the sweeper settles it: the plurality winner, or the default
99
+ on a tie or an empty ballot, stamped `resolvedHow: "deadline"`. Silence becomes
100
+ a decision rather than a stall. A reminder lands in the thread a day before.
101
+
102
+ That is why the default is required, and why it has to be the answer you can
103
+ live with.
@@ -115,6 +115,17 @@ You have write access. Use it in proportion:
115
115
 
116
116
  Fixing something you do not fully understand is worse than flagging it.
117
117
 
118
+ **A finding that turns on a product choice is neither.** When the diff is
119
+ defensible and the disagreement is really "should the product do X or Y", check
120
+ `list_decisions` first: if it was settled, cite the resolution and the diff is
121
+ either right or wrong on the record. If it was never settled, say so in the
122
+ verdict and name the fork — do NOT settle it yourself. `resolve_decision` is
123
+ refused in review mode for that reason: a review runs on a fixed budget against
124
+ a diff that already exists, and settling a decision writes that answer into
125
+ every card it was blocking. Raising one is fine if the fork is real; the author
126
+ or a moderator settles it. See
127
+ [../conveyor-plan/references/product-decisions.md](../conveyor-plan/references/product-decisions.md).
128
+
118
129
  ## The verdict
119
130
 
120
131
  > **Environment — the tools differ, and only one pair exists per surface.**
@@ -144,6 +144,14 @@ is still unknown — including what evidence would resolve it. Then move it to
144
144
  `Open`. "Needs a repro with the console open" is an actionable handoff;
145
145
  "couldn't reproduce" is not.
146
146
 
147
+ **When the unknown is a product choice, not missing evidence**, the handoff is
148
+ a decision rather than a triage note. "The code does X, the reporter expected
149
+ Y, and both are defensible" is not something more logs will settle — raise it
150
+ with `create_decision`, two or three options written GIVEN / WHEN / THEN, and
151
+ link this card as impacted so it un-blocks the moment the question is answered.
152
+ See
153
+ [../conveyor-plan/references/product-decisions.md](../conveyor-plan/references/product-decisions.md).
154
+
147
155
  **Cancelling:** explain the actual behavior, then cancel.
148
156
 
149
157
  ## 7. File at least one suggestion — always
@@ -38,9 +38,22 @@ and let Conveyor's own automation do the linking.
38
38
  it (post your context to its chat) rather than forking a duplicate.
39
39
  - **Classify**: buildable work → `mcp__conveyor__create_task`; an
40
40
  idea/improvement you are NOT committing to build →
41
- `mcp__conveyor__create_suggestion`; incidents (production breakage) are
42
- filed by monitoring and users through Conveyor's incident tooling — you
43
- will usually *work* incident cards, not create them.
41
+ `mcp__conveyor__create_suggestion`; a product fork that has to be settled
42
+ before the work can be planned correctly → `mcp__conveyor__create_decision`;
43
+ incidents
44
+ (production breakage) are filed by monitoring and users through Conveyor's
45
+ incident tooling — you will usually *work* incident cards, not create them.
46
+ - **Decisions**: a decision card holds one question, two or three options
47
+ written GIVEN / WHEN / THEN, a default that wins on silence, and a deadline.
48
+ It BLOCKS the cards it names until it settles, and un-blocks them the moment
49
+ it does — the answer is written into each of their plans and into the owning
50
+ tags' overviews, so the next agent finds it without knowing the decision
51
+ existed. Search with `mcp__conveyor__list_decisions` BEFORE planning in an
52
+ area: a settled
53
+ decision is the answer, and re-asking it is the waste the type exists to
54
+ stop. Most forks are not decisions — an assumption you can simply state
55
+ belongs in the plan as a GIVEN / WHEN / THEN line. The full bar is in
56
+ [../conveyor-plan/references/product-decisions.md](../conveyor-plan/references/product-decisions.md).
44
57
  - **Mechanics**: `create_task` takes the title, description, `plan`
45
58
  (markdown), and optional status/tags; cards start in `Planning`. Every
46
59
  status change you make goes through `mcp__conveyor__update_task`