agent-conveyor 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -16
- package/dist/cli/typescript-runtime.js +1305 -20
- package/dist/cli/typescript-runtime.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/app-autonomy.d.ts +1 -0
- package/dist/runtime/app-autonomy.js +16 -0
- package/dist/runtime/app-autonomy.js.map +1 -1
- package/dist/runtime/campaigns.d.ts +197 -0
- package/dist/runtime/campaigns.js +438 -0
- package/dist/runtime/campaigns.js.map +1 -0
- package/dist/runtime/manager-permissions.js +1 -1
- package/dist/runtime/manager-permissions.js.map +1 -1
- package/dist/state/schema-v23.js +87 -1
- package/dist/state/schema-v23.js.map +1 -1
- package/dist/state/sqlite-contract.d.ts +1 -1
- package/dist/state/sqlite-contract.js +13 -1
- package/dist/state/sqlite-contract.js.map +1 -1
- package/docs/manager-recipes.md +182 -0
- package/package.json +1 -1
- package/skills/manage-codex-workers/SKILL.md +143 -8
- package/skills/manage-codex-workers/agents/openai.yaml +2 -2
|
@@ -5,8 +5,9 @@ import { homedir, tmpdir } from "node:os";
|
|
|
5
5
|
import { dirname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { taskAuditSync } from "../runtime/audit.js";
|
|
8
|
-
import { appAutopilotPlanSync, appLoopStatusSync, appWakeupDispatchPlanSync, appWakeupPlanSync, directInboxPollCommand, } from "../runtime/app-autonomy.js";
|
|
8
|
+
import { appAutopilotPlanSync, appLoopStatusSync, appWakeupDispatchPlanSync, appWakeupPlanSync, directInboxPollCommand, visibleSessionProtocolLines, } from "../runtime/app-autonomy.js";
|
|
9
9
|
import { classifyBusyWait, classifyStartupOutput } from "../runtime/classify.js";
|
|
10
|
+
import { addCampaignWorkerSlotSync, campaignDashboardSync, campaignStatusSync, createCampaignAssignmentSync, createCampaignSync, recordCampaignAssetReceiptSync, updateCampaignWorkerSlotLifecycleSync, upsertCampaignChannelBriefSync, } from "../runtime/campaigns.js";
|
|
10
11
|
import { exportTaskSync } from "../runtime/export.js";
|
|
11
12
|
import { ingestSessionSync } from "../runtime/ingest.js";
|
|
12
13
|
import { acceptanceCriteriaForTaskSync, loopEvidenceCriterion, recordAdversarialLoopEvidenceSync, recordLoopEvidenceSync, recordVisualDiffLoopEvidenceSync, } from "../runtime/loop-evidence.js";
|
|
@@ -134,6 +135,12 @@ export function runTypescriptRuntimeCommand(options) {
|
|
|
134
135
|
if (parsed.command === "app-wakeup-record-delivery") {
|
|
135
136
|
return runAppWakeupRecordDeliveryCommand(parsed, options);
|
|
136
137
|
}
|
|
138
|
+
if (parsed.command === "app-worker-rotation-plan") {
|
|
139
|
+
return runAppWorkerRotationPlanCommand(parsed, options);
|
|
140
|
+
}
|
|
141
|
+
if (parsed.command === "app-worker-rotation-record") {
|
|
142
|
+
return runAppWorkerRotationRecordCommand(parsed, options);
|
|
143
|
+
}
|
|
137
144
|
if (parsed.command === "app-autopilot") {
|
|
138
145
|
return runAppAutopilotCommand(parsed, options);
|
|
139
146
|
}
|
|
@@ -146,6 +153,9 @@ export function runTypescriptRuntimeCommand(options) {
|
|
|
146
153
|
if (parsed.command === "tasks") {
|
|
147
154
|
return runTasksCommand(parsed, options);
|
|
148
155
|
}
|
|
156
|
+
if (parsed.command === "campaign") {
|
|
157
|
+
return runCampaignCommand(parsed, options);
|
|
158
|
+
}
|
|
149
159
|
if (parsed.command === "start") {
|
|
150
160
|
return runLegacyStartCommand(parsed, options);
|
|
151
161
|
}
|
|
@@ -368,10 +378,25 @@ function commandHelpText(program, command) {
|
|
|
368
378
|
` ${program} criteria my-task --satisfy 1 --proof "File exists" --evidence-json '{"artifact":{"path":"docs/note.md"}}' --path /tmp/work/workerctl.db`,
|
|
369
379
|
],
|
|
370
380
|
"finish-task": [
|
|
371
|
-
`usage: ${program} finish-task <task> --reason <reason> [--require-criteria-audit] ${path}`,
|
|
381
|
+
`usage: ${program} finish-task <task> --reason <reason> [--require-criteria-audit] ${path} [--json]`,
|
|
372
382
|
"",
|
|
373
383
|
"Examples:",
|
|
374
|
-
` ${program} finish-task my-task --reason "Accepted criteria satisfied" --require-criteria-audit --path /tmp/work/workerctl.db`,
|
|
384
|
+
` ${program} finish-task my-task --reason "Accepted criteria satisfied" --require-criteria-audit --path /tmp/work/workerctl.db --json`,
|
|
385
|
+
],
|
|
386
|
+
campaign: [
|
|
387
|
+
`usage: ${program} campaign <create|add-slot|attach-slot|rotate-slot|archive-slot|brief|assign|asset|status|dashboard> --name <campaign> [options] ${path} [--json]`,
|
|
388
|
+
"",
|
|
389
|
+
"Examples:",
|
|
390
|
+
` ${program} campaign create --name launch --objective "Create launch assets" --metadata-json '{"owner":"ops"}' --json`,
|
|
391
|
+
` ${program} campaign add-slot --name launch --slot-key tiktok --role-label "TikTok worker" --channel tiktok --thread-id thread-1 --state active --json`,
|
|
392
|
+
` ${program} campaign attach-slot --name launch --slot campaign-slot-id --session-id session-worker --thread-id thread-1 --state active --json`,
|
|
393
|
+
` ${program} campaign rotate-slot --name launch --slot campaign-slot-id --expected-thread-id thread-1 --thread-id thread-2 --thread-title "TikTok Worker 2" --json`,
|
|
394
|
+
` ${program} campaign archive-slot --name launch --slot campaign-slot-id --expected-thread-id thread-2 --json`,
|
|
395
|
+
` ${program} campaign brief --name launch --channel tiktok --brief-json '{"format":"9:16"}' --json`,
|
|
396
|
+
` ${program} campaign assign --name launch --slot campaign-slot-id --title "Draft hooks" --instructions "Create hooks" --status active --json`,
|
|
397
|
+
` ${program} campaign asset --name launch --slot campaign-slot-id --assignment campaign-assignment-id --asset-type copy --title "Hooks v1" --status needs_review --json`,
|
|
398
|
+
` ${program} campaign status --name launch --json`,
|
|
399
|
+
` ${program} campaign dashboard --name launch --json`,
|
|
375
400
|
],
|
|
376
401
|
"manager-ack": [
|
|
377
402
|
`usage: ${program} manager-ack <task> --from-stdin ${path}`,
|
|
@@ -399,6 +424,21 @@ function commandHelpText(program, command) {
|
|
|
399
424
|
` ${program} app-autopilot status dogfood --path /tmp/work/workerctl.db`,
|
|
400
425
|
` ${program} app-autopilot stop dogfood --path /tmp/work/workerctl.db --json`,
|
|
401
426
|
],
|
|
427
|
+
"app-worker-rotation-plan": [
|
|
428
|
+
`usage: ${program} app-worker-rotation-plan <task> --old-worker-thread-id ID [--require-handoff] [--reason TEXT] ${path} [--json]`,
|
|
429
|
+
"",
|
|
430
|
+
"Emit Codex app actions for replacing a worker thread with a fresh thread.",
|
|
431
|
+
"The plan fails closed unless the old thread id exactly matches the active bound worker session; it never authorizes archiving a manager or unrelated thread.",
|
|
432
|
+
"",
|
|
433
|
+
"Examples:",
|
|
434
|
+
` ${program} app-worker-rotation-plan dogfood --old-worker-thread-id thread-old --require-handoff --path /tmp/work/workerctl.db --json`,
|
|
435
|
+
],
|
|
436
|
+
"app-worker-rotation-record": [
|
|
437
|
+
`usage: ${program} app-worker-rotation-record <task> --old-worker-thread-id OLD --new-worker-thread-id NEW [--new-worker-thread-title TITLE] --archive-status archived|blocked [--reason TEXT] ${path} [--json]`,
|
|
438
|
+
"",
|
|
439
|
+
"Record the Codex app worker-thread rotation after the app layer creates the new worker thread and archives or blocks on the old one.",
|
|
440
|
+
"The command re-checks active binding ownership before updating the worker session to the new thread id.",
|
|
441
|
+
],
|
|
402
442
|
pair: [
|
|
403
443
|
`usage: ${program} pair --task <task> --worker-name <worker> --manager-name <manager> [options] ${path}`,
|
|
404
444
|
"",
|
|
@@ -431,6 +471,26 @@ function commandHelpText(program, command) {
|
|
|
431
471
|
};
|
|
432
472
|
return linesByCommand[command] ?? [`usage: ${program} ${command} [-h] [options]`];
|
|
433
473
|
}
|
|
474
|
+
const CAMPAIGN_ACTIONS = new Set(["create", "add-slot", "attach-slot", "rotate-slot", "archive-slot", "brief", "assign", "asset", "status", "dashboard"]);
|
|
475
|
+
const CAMPAIGN_STRING_FLAGS = {
|
|
476
|
+
"--artifact-path": "artifactPath",
|
|
477
|
+
"--asset-type": "assetType",
|
|
478
|
+
"--assignment": "assignment",
|
|
479
|
+
"--brief-json": "briefJson",
|
|
480
|
+
"--channel": "channel",
|
|
481
|
+
"--expected-thread-id": "expectedThreadId",
|
|
482
|
+
"--instructions": "instructions",
|
|
483
|
+
"--objective": "objective",
|
|
484
|
+
"--prompt-summary": "promptSummary",
|
|
485
|
+
"--review-notes": "reviewNotes",
|
|
486
|
+
"--role-label": "roleLabel",
|
|
487
|
+
"--session-id": "sessionId",
|
|
488
|
+
"--slot": "slot",
|
|
489
|
+
"--slot-key": "slotKey",
|
|
490
|
+
"--thread-id": "threadId",
|
|
491
|
+
"--thread-title": "threadTitle",
|
|
492
|
+
"--title": "title",
|
|
493
|
+
};
|
|
434
494
|
function parseRuntimeArgs(args, env) {
|
|
435
495
|
const flags = {
|
|
436
496
|
format: "timeline",
|
|
@@ -439,8 +499,12 @@ function parseRuntimeArgs(args, env) {
|
|
|
439
499
|
includeTranscripts: false,
|
|
440
500
|
all: false,
|
|
441
501
|
action: null,
|
|
502
|
+
artifactPath: null,
|
|
503
|
+
assetType: null,
|
|
504
|
+
assignment: null,
|
|
442
505
|
asRole: "all",
|
|
443
506
|
attempts: false,
|
|
507
|
+
briefJson: null,
|
|
444
508
|
json: false,
|
|
445
509
|
activeOnly: false,
|
|
446
510
|
actor: null,
|
|
@@ -454,7 +518,9 @@ function parseRuntimeArgs(args, env) {
|
|
|
454
518
|
blocker: null,
|
|
455
519
|
busyWaitSeconds: DEFAULT_BUSY_WAIT_SECONDS,
|
|
456
520
|
candidate: null,
|
|
521
|
+
campaignName: null,
|
|
457
522
|
check: null,
|
|
523
|
+
channel: null,
|
|
458
524
|
classifyPrompt: null,
|
|
459
525
|
workerCodexAppThreadId: null,
|
|
460
526
|
workerCodexAppThreadTitle: null,
|
|
@@ -480,6 +546,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
480
546
|
epilogueStatus: false,
|
|
481
547
|
epilogueStep: null,
|
|
482
548
|
eventType: null,
|
|
549
|
+
expectedThreadId: null,
|
|
483
550
|
failureMode: null,
|
|
484
551
|
file: null,
|
|
485
552
|
finishRun: null,
|
|
@@ -487,6 +554,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
487
554
|
fromText: null,
|
|
488
555
|
fromWorkerResponse: null,
|
|
489
556
|
goal: null,
|
|
557
|
+
instructions: null,
|
|
490
558
|
keepLatest: 20,
|
|
491
559
|
key: "C-c",
|
|
492
560
|
list: false,
|
|
@@ -512,7 +580,10 @@ function parseRuntimeArgs(args, env) {
|
|
|
512
580
|
require: false,
|
|
513
581
|
requireHandoff: false,
|
|
514
582
|
result: null,
|
|
583
|
+
reviewNotes: null,
|
|
584
|
+
roleLabel: null,
|
|
515
585
|
review: false,
|
|
586
|
+
sessionId: null,
|
|
516
587
|
sessionRole: null,
|
|
517
588
|
sessionState: null,
|
|
518
589
|
show: null,
|
|
@@ -523,9 +594,13 @@ function parseRuntimeArgs(args, env) {
|
|
|
523
594
|
statuses: [],
|
|
524
595
|
statusStaleSeconds: DEFAULT_STATUS_STALE_SECONDS,
|
|
525
596
|
submitRole: null,
|
|
597
|
+
slot: null,
|
|
598
|
+
slotKey: null,
|
|
526
599
|
subtype: null,
|
|
527
600
|
summary: null,
|
|
528
601
|
source: null,
|
|
602
|
+
objective: null,
|
|
603
|
+
promptSummary: null,
|
|
529
604
|
proof: null,
|
|
530
605
|
purpose: null,
|
|
531
606
|
quietAfterCycles: 3,
|
|
@@ -568,8 +643,14 @@ function parseRuntimeArgs(args, env) {
|
|
|
568
643
|
dispatchReceipt: null,
|
|
569
644
|
force: false,
|
|
570
645
|
message: null,
|
|
646
|
+
newWorkerThreadId: null,
|
|
647
|
+
newWorkerThreadTitle: null,
|
|
648
|
+
oldWorkerThreadId: null,
|
|
571
649
|
reason: null,
|
|
572
650
|
threadId: null,
|
|
651
|
+
threadTitle: null,
|
|
652
|
+
title: null,
|
|
653
|
+
archiveStatus: null,
|
|
573
654
|
consumeNext: false,
|
|
574
655
|
wait: false,
|
|
575
656
|
requireAcks: false,
|
|
@@ -937,7 +1018,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
937
1018
|
}
|
|
938
1019
|
}
|
|
939
1020
|
else if (arg === "--require-handoff") {
|
|
940
|
-
if (command !== "manager-permission") {
|
|
1021
|
+
if (command !== "manager-permission" && command !== "app-worker-rotation-plan") {
|
|
941
1022
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --require-handoff", explicit, flags, task };
|
|
942
1023
|
}
|
|
943
1024
|
flags.requireHandoff = true;
|
|
@@ -1037,6 +1118,17 @@ function parseRuntimeArgs(args, env) {
|
|
|
1037
1118
|
flags.workerctlPath = value.value;
|
|
1038
1119
|
index += 1;
|
|
1039
1120
|
}
|
|
1121
|
+
else if (arg === "--campaign") {
|
|
1122
|
+
if (command !== "dashboard") {
|
|
1123
|
+
return { command, enabled, error: "Unsupported TypeScript runtime option: --campaign", explicit, flags, task };
|
|
1124
|
+
}
|
|
1125
|
+
const value = valueAfter(queue, index, arg);
|
|
1126
|
+
if (value.error) {
|
|
1127
|
+
return { command, enabled, error: value.error, explicit, flags, task };
|
|
1128
|
+
}
|
|
1129
|
+
flags.campaignName = value.value;
|
|
1130
|
+
index += 1;
|
|
1131
|
+
}
|
|
1040
1132
|
else if (arg === "--output") {
|
|
1041
1133
|
const value = valueAfter(queue, index, arg);
|
|
1042
1134
|
if (value.error) {
|
|
@@ -1166,7 +1258,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
1166
1258
|
index += 1;
|
|
1167
1259
|
}
|
|
1168
1260
|
else if (arg === "--metadata-json") {
|
|
1169
|
-
if (command !== "runs" && command !== "loop-evidence") {
|
|
1261
|
+
if (command !== "runs" && command !== "loop-evidence" && command !== "campaign") {
|
|
1170
1262
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --metadata-json", explicit, flags, task };
|
|
1171
1263
|
}
|
|
1172
1264
|
const value = valueAfter(queue, index, arg);
|
|
@@ -1232,6 +1324,14 @@ function parseRuntimeArgs(args, env) {
|
|
|
1232
1324
|
flags.names.push(value.value);
|
|
1233
1325
|
index += 1;
|
|
1234
1326
|
}
|
|
1327
|
+
else if (command === "campaign" && Object.prototype.hasOwnProperty.call(CAMPAIGN_STRING_FLAGS, arg)) {
|
|
1328
|
+
const value = valueAfter(queue, index, arg);
|
|
1329
|
+
if (value.error) {
|
|
1330
|
+
return { command, enabled, error: value.error, explicit, flags, task };
|
|
1331
|
+
}
|
|
1332
|
+
flags[CAMPAIGN_STRING_FLAGS[arg]] = value.value;
|
|
1333
|
+
index += 1;
|
|
1334
|
+
}
|
|
1235
1335
|
else if (arg === "--pid") {
|
|
1236
1336
|
const parsedValue = valueAfter(queue, index, arg);
|
|
1237
1337
|
if (parsedValue.error) {
|
|
@@ -1668,14 +1768,19 @@ function parseRuntimeArgs(args, env) {
|
|
|
1668
1768
|
index += 1;
|
|
1669
1769
|
}
|
|
1670
1770
|
else if (arg === "--objective") {
|
|
1671
|
-
if (command !== "manager-config") {
|
|
1771
|
+
if (command !== "manager-config" && command !== "campaign") {
|
|
1672
1772
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --objective", explicit, flags, task };
|
|
1673
1773
|
}
|
|
1674
1774
|
const value = valueAfter(queue, index, arg);
|
|
1675
1775
|
if (value.error) {
|
|
1676
1776
|
return { command, enabled, error: value.error, explicit, flags, task };
|
|
1677
1777
|
}
|
|
1678
|
-
|
|
1778
|
+
if (command === "manager-config") {
|
|
1779
|
+
flags.managerObjective = value.value;
|
|
1780
|
+
}
|
|
1781
|
+
else {
|
|
1782
|
+
flags.objective = value.value;
|
|
1783
|
+
}
|
|
1679
1784
|
index += 1;
|
|
1680
1785
|
}
|
|
1681
1786
|
else if (arg === "--recipe") {
|
|
@@ -1855,7 +1960,13 @@ function parseRuntimeArgs(args, env) {
|
|
|
1855
1960
|
index = queue.length;
|
|
1856
1961
|
}
|
|
1857
1962
|
else if (arg === "--reason") {
|
|
1858
|
-
if (command !== "finish-task"
|
|
1963
|
+
if (command !== "finish-task"
|
|
1964
|
+
&& command !== "stop-task"
|
|
1965
|
+
&& command !== "record-decision"
|
|
1966
|
+
&& command !== "compact-worker"
|
|
1967
|
+
&& command !== "app-wakeup-record-delivery"
|
|
1968
|
+
&& command !== "app-worker-rotation-plan"
|
|
1969
|
+
&& command !== "app-worker-rotation-record") {
|
|
1859
1970
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --reason", explicit, flags, task };
|
|
1860
1971
|
}
|
|
1861
1972
|
const value = valueAfter(queue, index, arg);
|
|
@@ -1887,8 +1998,19 @@ function parseRuntimeArgs(args, env) {
|
|
|
1887
1998
|
flags.deliveryStatus = value.value;
|
|
1888
1999
|
index += 1;
|
|
1889
2000
|
}
|
|
2001
|
+
else if (arg === "--archive-status") {
|
|
2002
|
+
if (command !== "app-worker-rotation-record") {
|
|
2003
|
+
return { command, enabled, error: "Unsupported TypeScript runtime option: --archive-status", explicit, flags, task };
|
|
2004
|
+
}
|
|
2005
|
+
const value = valueAfter(queue, index, arg);
|
|
2006
|
+
if (value.error) {
|
|
2007
|
+
return { command, enabled, error: value.error, explicit, flags, task };
|
|
2008
|
+
}
|
|
2009
|
+
flags.archiveStatus = value.value;
|
|
2010
|
+
index += 1;
|
|
2011
|
+
}
|
|
1890
2012
|
else if (arg === "--thread-id") {
|
|
1891
|
-
if (command !== "app-wakeup-record-delivery") {
|
|
2013
|
+
if (command !== "app-wakeup-record-delivery" && command !== "campaign") {
|
|
1892
2014
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --thread-id", explicit, flags, task };
|
|
1893
2015
|
}
|
|
1894
2016
|
const value = valueAfter(queue, index, arg);
|
|
@@ -1898,6 +2020,28 @@ function parseRuntimeArgs(args, env) {
|
|
|
1898
2020
|
flags.threadId = value.value;
|
|
1899
2021
|
index += 1;
|
|
1900
2022
|
}
|
|
2023
|
+
else if (arg === "--old-worker-thread-id" || arg === "--new-worker-thread-id" || arg === "--new-worker-thread-title") {
|
|
2024
|
+
if (command !== "app-worker-rotation-plan" && command !== "app-worker-rotation-record") {
|
|
2025
|
+
return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
|
|
2026
|
+
}
|
|
2027
|
+
if (arg !== "--old-worker-thread-id" && command !== "app-worker-rotation-record") {
|
|
2028
|
+
return { command, enabled, error: `Unsupported TypeScript runtime option: ${arg}`, explicit, flags, task };
|
|
2029
|
+
}
|
|
2030
|
+
const value = valueAfter(queue, index, arg);
|
|
2031
|
+
if (value.error) {
|
|
2032
|
+
return { command, enabled, error: value.error, explicit, flags, task };
|
|
2033
|
+
}
|
|
2034
|
+
if (arg === "--old-worker-thread-id") {
|
|
2035
|
+
flags.oldWorkerThreadId = value.value;
|
|
2036
|
+
}
|
|
2037
|
+
else if (arg === "--new-worker-thread-id") {
|
|
2038
|
+
flags.newWorkerThreadId = value.value;
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
flags.newWorkerThreadTitle = value.value;
|
|
2042
|
+
}
|
|
2043
|
+
index += 1;
|
|
2044
|
+
}
|
|
1901
2045
|
else if (arg === "--message") {
|
|
1902
2046
|
if (command !== "finish-task"
|
|
1903
2047
|
&& command !== "stop-task"
|
|
@@ -2189,6 +2333,9 @@ function parseRuntimeArgs(args, env) {
|
|
|
2189
2333
|
}
|
|
2190
2334
|
flags.statusState = value;
|
|
2191
2335
|
}
|
|
2336
|
+
else if (command === "campaign") {
|
|
2337
|
+
flags.statusState = value;
|
|
2338
|
+
}
|
|
2192
2339
|
else if (!isSessionState(value)) {
|
|
2193
2340
|
return { command, enabled, error: `Unsupported sessions state: ${value}`, explicit, flags, task };
|
|
2194
2341
|
}
|
|
@@ -2202,7 +2349,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
2202
2349
|
flags.epilogueStatus = true;
|
|
2203
2350
|
continue;
|
|
2204
2351
|
}
|
|
2205
|
-
if (command !== "criteria" && command !== "runs" && command !== "loop-evidence") {
|
|
2352
|
+
if (command !== "criteria" && command !== "runs" && command !== "loop-evidence" && command !== "campaign") {
|
|
2206
2353
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --status", explicit, flags, task };
|
|
2207
2354
|
}
|
|
2208
2355
|
const parsedValue = valueAfter(queue, index, arg);
|
|
@@ -2854,7 +3001,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
2854
3001
|
}
|
|
2855
3002
|
}
|
|
2856
3003
|
else if (command === "loop-evidence" && flags.subtype === null) {
|
|
2857
|
-
if (!["add", "visual-diff", "visual_diff", "adversarial-check", "adversarial_check"].includes(arg)) {
|
|
3004
|
+
if (!["add", "visual-diff", "visual_diff", "build-passed", "build_passed", "adversarial-check", "adversarial_check"].includes(arg)) {
|
|
2858
3005
|
return { command, enabled, error: `Unsupported loop-evidence action: ${arg}`, explicit, flags, task };
|
|
2859
3006
|
}
|
|
2860
3007
|
flags.subtype = arg;
|
|
@@ -2865,6 +3012,12 @@ function parseRuntimeArgs(args, env) {
|
|
|
2865
3012
|
}
|
|
2866
3013
|
flags.action = arg;
|
|
2867
3014
|
}
|
|
3015
|
+
else if (command === "campaign" && flags.action === null) {
|
|
3016
|
+
if (!CAMPAIGN_ACTIONS.has(arg)) {
|
|
3017
|
+
return { command, enabled, error: `Unsupported campaign action: ${arg}`, explicit, flags, task };
|
|
3018
|
+
}
|
|
3019
|
+
flags.action = arg;
|
|
3020
|
+
}
|
|
2868
3021
|
else if ((command === "qa-plan" || command === "qa-run") && flags.subtype === null) {
|
|
2869
3022
|
flags.subtype = arg;
|
|
2870
3023
|
}
|
|
@@ -3164,7 +3317,7 @@ function runLoopEvidenceCommand(parsed, options) {
|
|
|
3164
3317
|
}
|
|
3165
3318
|
const action = parsed.flags.subtype;
|
|
3166
3319
|
if (!action) {
|
|
3167
|
-
return unsupportedRuntimeResult(parsed, "loop-evidence requires an action: add, visual-diff, or adversarial-check.");
|
|
3320
|
+
return unsupportedRuntimeResult(parsed, "loop-evidence requires an action: add, visual-diff, build-passed, or adversarial-check.");
|
|
3168
3321
|
}
|
|
3169
3322
|
const task = requireTask(parsed);
|
|
3170
3323
|
if (!parsed.flags.loopRun) {
|
|
@@ -3191,6 +3344,24 @@ function runLoopEvidenceCommand(parsed, options) {
|
|
|
3191
3344
|
});
|
|
3192
3345
|
return jsonResult(result);
|
|
3193
3346
|
}
|
|
3347
|
+
if (action === "build-passed" || action === "build_passed") {
|
|
3348
|
+
if (parsed.flags.evidenceType && parsed.flags.evidenceType !== "build_passed") {
|
|
3349
|
+
return errorResult("loop-evidence build-passed records evidence_type=build_passed; omit --evidence-type or use build_passed.");
|
|
3350
|
+
}
|
|
3351
|
+
const result = recordLoopEvidenceSync(database, {
|
|
3352
|
+
artifactPath: parsed.flags.output,
|
|
3353
|
+
correlationId: parsed.flags.correlationId,
|
|
3354
|
+
evidenceType: "build_passed",
|
|
3355
|
+
iteration: parsed.flags.currentIteration,
|
|
3356
|
+
loopRunId: parsed.flags.loopRun,
|
|
3357
|
+
metadata: jsonObjectArg(parsed.flags.metadataJson, "--metadata-json"),
|
|
3358
|
+
proof: parsed.flags.proof,
|
|
3359
|
+
source,
|
|
3360
|
+
status: parsed.flags.statusState ?? "pass",
|
|
3361
|
+
task,
|
|
3362
|
+
});
|
|
3363
|
+
return jsonResult(result);
|
|
3364
|
+
}
|
|
3194
3365
|
if (action === "adversarial-check" || action === "adversarial_check") {
|
|
3195
3366
|
const result = recordAdversarialLoopEvidenceSync(database, {
|
|
3196
3367
|
artifactPath: parsed.flags.output,
|
|
@@ -3734,6 +3905,319 @@ function runAppWakeupRecordDeliveryCommand(parsed, options) {
|
|
|
3734
3905
|
database.close();
|
|
3735
3906
|
}
|
|
3736
3907
|
}
|
|
3908
|
+
function runAppWorkerRotationPlanCommand(parsed, options) {
|
|
3909
|
+
const taskName = requireTask(parsed);
|
|
3910
|
+
if (!parsed.flags.oldWorkerThreadId) {
|
|
3911
|
+
return errorResult("app-worker-rotation-plan requires --old-worker-thread-id.");
|
|
3912
|
+
}
|
|
3913
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
3914
|
+
try {
|
|
3915
|
+
const timestamp = nowIsoSeconds(options);
|
|
3916
|
+
const dbPath = runtimeDbPath(parsed, options);
|
|
3917
|
+
const plan = appWorkerRotationPlanSync(database, {
|
|
3918
|
+
dbPath,
|
|
3919
|
+
now: timestamp,
|
|
3920
|
+
oldWorkerThreadId: parsed.flags.oldWorkerThreadId,
|
|
3921
|
+
reason: parsed.flags.reason,
|
|
3922
|
+
requireHandoff: parsed.flags.requireHandoff,
|
|
3923
|
+
taskName,
|
|
3924
|
+
});
|
|
3925
|
+
const eventId = emitTelemetrySync(database, {
|
|
3926
|
+
actor: "manager",
|
|
3927
|
+
attributes: {
|
|
3928
|
+
actions: plan.actions.map((action) => ({
|
|
3929
|
+
status: action.status,
|
|
3930
|
+
thread_id: action.thread.id,
|
|
3931
|
+
type: action.type,
|
|
3932
|
+
})),
|
|
3933
|
+
blockers: plan.blockers,
|
|
3934
|
+
eligible: plan.eligible,
|
|
3935
|
+
handoff_id: plan.handoff?.id ?? null,
|
|
3936
|
+
old_worker_thread_id: parsed.flags.oldWorkerThreadId,
|
|
3937
|
+
reason: parsed.flags.reason,
|
|
3938
|
+
},
|
|
3939
|
+
correlation: {
|
|
3940
|
+
command: "app-worker-rotation-plan",
|
|
3941
|
+
old_worker_thread_id: parsed.flags.oldWorkerThreadId,
|
|
3942
|
+
},
|
|
3943
|
+
eventType: "app_worker_rotation_planned",
|
|
3944
|
+
severity: plan.eligible ? "info" : "warning",
|
|
3945
|
+
summary: `Codex app worker rotation ${plan.eligible ? "planned" : "blocked"} for ${plan.task.name}.`,
|
|
3946
|
+
taskId: plan.task.id,
|
|
3947
|
+
timestamp,
|
|
3948
|
+
});
|
|
3949
|
+
const output = {
|
|
3950
|
+
...plan,
|
|
3951
|
+
receipt: {
|
|
3952
|
+
event_id: eventId,
|
|
3953
|
+
event_type: "app_worker_rotation_planned",
|
|
3954
|
+
recorded_at: timestamp,
|
|
3955
|
+
},
|
|
3956
|
+
};
|
|
3957
|
+
if (parsed.flags.json) {
|
|
3958
|
+
return jsonResult(output);
|
|
3959
|
+
}
|
|
3960
|
+
return {
|
|
3961
|
+
exitCode: 0,
|
|
3962
|
+
handled: true,
|
|
3963
|
+
stdout: renderAppWorkerRotationPlanText(output),
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
finally {
|
|
3967
|
+
database.close();
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
function runAppWorkerRotationRecordCommand(parsed, options) {
|
|
3971
|
+
const taskName = requireTask(parsed);
|
|
3972
|
+
if (!parsed.flags.oldWorkerThreadId) {
|
|
3973
|
+
return errorResult("app-worker-rotation-record requires --old-worker-thread-id.");
|
|
3974
|
+
}
|
|
3975
|
+
if (!parsed.flags.newWorkerThreadId) {
|
|
3976
|
+
return errorResult("app-worker-rotation-record requires --new-worker-thread-id.");
|
|
3977
|
+
}
|
|
3978
|
+
const archiveStatus = parseAppWorkerArchiveStatus(parsed.flags.archiveStatus);
|
|
3979
|
+
if (archiveStatus instanceof Error) {
|
|
3980
|
+
return errorResult(archiveStatus.message);
|
|
3981
|
+
}
|
|
3982
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
3983
|
+
try {
|
|
3984
|
+
const timestamp = nowIsoSeconds(options);
|
|
3985
|
+
const dbPath = runtimeDbPath(parsed, options);
|
|
3986
|
+
const plan = appWorkerRotationPlanSync(database, {
|
|
3987
|
+
dbPath,
|
|
3988
|
+
now: timestamp,
|
|
3989
|
+
oldWorkerThreadId: parsed.flags.oldWorkerThreadId,
|
|
3990
|
+
reason: parsed.flags.reason,
|
|
3991
|
+
requireHandoff: false,
|
|
3992
|
+
taskName,
|
|
3993
|
+
});
|
|
3994
|
+
if (!plan.eligible) {
|
|
3995
|
+
throw new Error(`Cannot record worker rotation; active worker ownership check failed: ${plan.blockers.join(", ")}`);
|
|
3996
|
+
}
|
|
3997
|
+
if (archiveStatus === "archived") {
|
|
3998
|
+
database.prepare(`
|
|
3999
|
+
update sessions
|
|
4000
|
+
set codex_app_thread_id = ?, codex_app_thread_title = ?, last_heartbeat_at = null
|
|
4001
|
+
where id = ? and role = 'worker'
|
|
4002
|
+
`).run(parsed.flags.newWorkerThreadId, parsed.flags.newWorkerThreadTitle, plan.old_worker.session_id);
|
|
4003
|
+
}
|
|
4004
|
+
const eventId = emitTelemetrySync(database, {
|
|
4005
|
+
actor: "manager",
|
|
4006
|
+
attributes: {
|
|
4007
|
+
archive_status: archiveStatus,
|
|
4008
|
+
binding_id: plan.guard.binding_id,
|
|
4009
|
+
handoff_id: plan.handoff?.id ?? null,
|
|
4010
|
+
new_worker_thread_id: parsed.flags.newWorkerThreadId,
|
|
4011
|
+
new_worker_thread_title: parsed.flags.newWorkerThreadTitle,
|
|
4012
|
+
old_worker_session_id: plan.old_worker.session_id,
|
|
4013
|
+
old_worker_thread_id: parsed.flags.oldWorkerThreadId,
|
|
4014
|
+
reason: parsed.flags.reason,
|
|
4015
|
+
session_updated: archiveStatus === "archived",
|
|
4016
|
+
},
|
|
4017
|
+
correlation: {
|
|
4018
|
+
command: "app-worker-rotation-record",
|
|
4019
|
+
new_worker_thread_id: parsed.flags.newWorkerThreadId,
|
|
4020
|
+
old_worker_thread_id: parsed.flags.oldWorkerThreadId,
|
|
4021
|
+
},
|
|
4022
|
+
eventType: "app_worker_rotation_recorded",
|
|
4023
|
+
severity: archiveStatus === "archived" ? "info" : "warning",
|
|
4024
|
+
summary: `Codex app worker rotation ${archiveStatus} for ${plan.task.name}.`,
|
|
4025
|
+
taskId: plan.task.id,
|
|
4026
|
+
timestamp,
|
|
4027
|
+
});
|
|
4028
|
+
const output = {
|
|
4029
|
+
archive: {
|
|
4030
|
+
old_worker_thread_id: parsed.flags.oldWorkerThreadId,
|
|
4031
|
+
status: archiveStatus,
|
|
4032
|
+
},
|
|
4033
|
+
guard: plan.guard,
|
|
4034
|
+
new_worker: {
|
|
4035
|
+
codex_app_thread_id: parsed.flags.newWorkerThreadId,
|
|
4036
|
+
codex_app_thread_title: parsed.flags.newWorkerThreadTitle,
|
|
4037
|
+
session_id: plan.old_worker.session_id,
|
|
4038
|
+
session_updated: archiveStatus === "archived",
|
|
4039
|
+
},
|
|
4040
|
+
old_worker: plan.old_worker,
|
|
4041
|
+
receipt: {
|
|
4042
|
+
event_id: eventId,
|
|
4043
|
+
event_type: "app_worker_rotation_recorded",
|
|
4044
|
+
recorded_at: timestamp,
|
|
4045
|
+
},
|
|
4046
|
+
task: plan.task,
|
|
4047
|
+
};
|
|
4048
|
+
if (parsed.flags.json) {
|
|
4049
|
+
return jsonResult(output);
|
|
4050
|
+
}
|
|
4051
|
+
return {
|
|
4052
|
+
exitCode: 0,
|
|
4053
|
+
handled: true,
|
|
4054
|
+
stdout: [
|
|
4055
|
+
`App worker rotation ${archiveStatus} for ${plan.task.name}.`,
|
|
4056
|
+
`Old worker thread: ${parsed.flags.oldWorkerThreadId}`,
|
|
4057
|
+
`New worker thread: ${parsed.flags.newWorkerThreadId}`,
|
|
4058
|
+
`Receipt: ${eventId}`,
|
|
4059
|
+
].join("\n") + "\n",
|
|
4060
|
+
};
|
|
4061
|
+
}
|
|
4062
|
+
finally {
|
|
4063
|
+
database.close();
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
function parseAppWorkerArchiveStatus(value) {
|
|
4067
|
+
if (value === "archived" || value === "blocked") {
|
|
4068
|
+
return value;
|
|
4069
|
+
}
|
|
4070
|
+
return new Error("app-worker-rotation-record requires --archive-status archived|blocked");
|
|
4071
|
+
}
|
|
4072
|
+
function appWorkerRotationPlanSync(database, options) {
|
|
4073
|
+
const task = taskRowForPair(database, options.taskName);
|
|
4074
|
+
if (task === null) {
|
|
4075
|
+
throw new Error(`Unknown task: ${options.taskName}`);
|
|
4076
|
+
}
|
|
4077
|
+
const binding = activeBindingForTaskSync(database, task.name);
|
|
4078
|
+
const worker = sessionRow(database, binding.worker_session_name, "worker");
|
|
4079
|
+
const manager = sessionRow(database, binding.manager_session_name, "manager");
|
|
4080
|
+
const handoff = latestWorkerHandoffFullSync(database, task.id);
|
|
4081
|
+
const blockers = [];
|
|
4082
|
+
const exactThreadMatch = worker.codex_app_thread_id === options.oldWorkerThreadId;
|
|
4083
|
+
if (binding.state !== "active") {
|
|
4084
|
+
blockers.push("active_binding_not_active");
|
|
4085
|
+
}
|
|
4086
|
+
if (worker.state !== "active") {
|
|
4087
|
+
blockers.push("worker_session_not_active");
|
|
4088
|
+
}
|
|
4089
|
+
if (worker.tmux_session !== null) {
|
|
4090
|
+
blockers.push("worker_session_is_tmux_backed");
|
|
4091
|
+
}
|
|
4092
|
+
if (!worker.codex_app_thread_id) {
|
|
4093
|
+
blockers.push("missing_worker_codex_app_thread_id");
|
|
4094
|
+
}
|
|
4095
|
+
if (!exactThreadMatch) {
|
|
4096
|
+
blockers.push("old_worker_thread_id_mismatch");
|
|
4097
|
+
}
|
|
4098
|
+
if (manager.id === worker.id || manager.codex_app_thread_id === worker.codex_app_thread_id) {
|
|
4099
|
+
blockers.push("manager_worker_thread_not_distinct");
|
|
4100
|
+
}
|
|
4101
|
+
if (options.requireHandoff) {
|
|
4102
|
+
if (handoff === null) {
|
|
4103
|
+
blockers.push("missing_worker_handoff");
|
|
4104
|
+
}
|
|
4105
|
+
else if (handoff.worker_session_id !== worker.id) {
|
|
4106
|
+
blockers.push("handoff_worker_session_mismatch");
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
const guard = {
|
|
4110
|
+
active_binding: binding.state === "active",
|
|
4111
|
+
binding_id: binding.binding_id,
|
|
4112
|
+
exact_thread_match: exactThreadMatch,
|
|
4113
|
+
expected_old_worker_thread_id: options.oldWorkerThreadId,
|
|
4114
|
+
manager_session_id: manager.id,
|
|
4115
|
+
manager_thread_id: manager.codex_app_thread_id,
|
|
4116
|
+
require_handoff: options.requireHandoff,
|
|
4117
|
+
role: "worker",
|
|
4118
|
+
task_id: task.id,
|
|
4119
|
+
worker_session_id: worker.id,
|
|
4120
|
+
worker_state: worker.state,
|
|
4121
|
+
};
|
|
4122
|
+
const eligible = blockers.length === 0;
|
|
4123
|
+
const replacementTitle = replacementWorkerThreadTitle(task.name, worker.codex_app_thread_title);
|
|
4124
|
+
const recordCommand = `${conveyorPollInvocation()} app-worker-rotation-record ${shellQuote(task.name)} --old-worker-thread-id ${shellQuote(options.oldWorkerThreadId)} --new-worker-thread-id <new.thread.id> --new-worker-thread-title <new.thread.title> --archive-status archived --path ${shellQuote(options.dbPath)} --json`;
|
|
4125
|
+
return {
|
|
4126
|
+
actions: eligible
|
|
4127
|
+
? [
|
|
4128
|
+
{
|
|
4129
|
+
guard,
|
|
4130
|
+
prompt: replacementWorkerPrompt({
|
|
4131
|
+
dbPath: options.dbPath,
|
|
4132
|
+
handoff,
|
|
4133
|
+
reason: options.reason,
|
|
4134
|
+
taskName: task.name,
|
|
4135
|
+
}),
|
|
4136
|
+
role: "worker",
|
|
4137
|
+
send_ready: true,
|
|
4138
|
+
status: "ready_to_create",
|
|
4139
|
+
thread: { id: null, title: replacementTitle },
|
|
4140
|
+
type: "create_replacement_worker_thread",
|
|
4141
|
+
},
|
|
4142
|
+
{
|
|
4143
|
+
guard,
|
|
4144
|
+
role: "worker",
|
|
4145
|
+
send_ready: true,
|
|
4146
|
+
status: "ready_to_archive",
|
|
4147
|
+
thread: { id: worker.codex_app_thread_id, title: worker.codex_app_thread_title },
|
|
4148
|
+
type: "archive_old_worker_thread",
|
|
4149
|
+
},
|
|
4150
|
+
]
|
|
4151
|
+
: [],
|
|
4152
|
+
blockers,
|
|
4153
|
+
eligible,
|
|
4154
|
+
guard,
|
|
4155
|
+
handoff: handoff === null
|
|
4156
|
+
? null
|
|
4157
|
+
: {
|
|
4158
|
+
created_at: handoff.created_at,
|
|
4159
|
+
id: handoff.id,
|
|
4160
|
+
next_steps: handoff.next_steps,
|
|
4161
|
+
summary: handoff.summary,
|
|
4162
|
+
worker_session_id: handoff.worker_session_id,
|
|
4163
|
+
},
|
|
4164
|
+
old_worker: {
|
|
4165
|
+
codex_app_thread_id: worker.codex_app_thread_id,
|
|
4166
|
+
codex_app_thread_title: worker.codex_app_thread_title,
|
|
4167
|
+
name: worker.name,
|
|
4168
|
+
session_id: worker.id,
|
|
4169
|
+
},
|
|
4170
|
+
record_command: eligible ? recordCommand : null,
|
|
4171
|
+
task: { id: task.id, name: task.name },
|
|
4172
|
+
};
|
|
4173
|
+
}
|
|
4174
|
+
function replacementWorkerThreadTitle(taskName, oldTitle) {
|
|
4175
|
+
const base = oldTitle && oldTitle.trim().length > 0 ? oldTitle.trim() : `${taskName} worker`;
|
|
4176
|
+
return `${base} fresh`;
|
|
4177
|
+
}
|
|
4178
|
+
function replacementWorkerPrompt(options) {
|
|
4179
|
+
const handoffLines = options.handoff === null
|
|
4180
|
+
? ["No saved handoff was required for this rotation plan."]
|
|
4181
|
+
: [
|
|
4182
|
+
`Saved handoff id: ${String(options.handoff.id)}`,
|
|
4183
|
+
`Saved handoff summary: ${String(options.handoff.summary)}`,
|
|
4184
|
+
`Saved handoff next steps: ${JSON.stringify(options.handoff.next_steps ?? [])}`,
|
|
4185
|
+
];
|
|
4186
|
+
return [
|
|
4187
|
+
"Use the manage-codex-workers skill.",
|
|
4188
|
+
`You are the replacement Codex app worker thread for task ${options.taskName}.`,
|
|
4189
|
+
options.reason ? `Rotation reason: ${options.reason}` : "Rotation reason: fresh worker context for Codex app usage.",
|
|
4190
|
+
...handoffLines,
|
|
4191
|
+
"",
|
|
4192
|
+
"Resume from the saved handoff and continue through Conveyor only. Do not rely on the archived worker thread for context beyond the handoff above.",
|
|
4193
|
+
...visibleSessionProtocolLines("worker"),
|
|
4194
|
+
`Run: ${disposableAppHeartbeatCommand("worker", options.taskName, options.dbPath)}`,
|
|
4195
|
+
`If the heartbeat output asks for direct inbox polling, run: ${sessionPollCommand("worker", options.taskName, options.dbPath)}`,
|
|
4196
|
+
"If no item is consumed, stop after a one-line idle receipt.",
|
|
4197
|
+
].join("\n");
|
|
4198
|
+
}
|
|
4199
|
+
function renderAppWorkerRotationPlanText(plan) {
|
|
4200
|
+
const lines = [
|
|
4201
|
+
`App worker rotation for ${plan.task.name}: ${plan.eligible ? "ready" : "blocked"}`,
|
|
4202
|
+
`Old worker thread: ${plan.old_worker.codex_app_thread_id ?? "(missing)"}`,
|
|
4203
|
+
];
|
|
4204
|
+
if (plan.blockers.length > 0) {
|
|
4205
|
+
lines.push(`Blockers: ${plan.blockers.join(", ")}`);
|
|
4206
|
+
}
|
|
4207
|
+
for (const action of plan.actions) {
|
|
4208
|
+
lines.push(`${action.type}: ${action.status}`);
|
|
4209
|
+
if (action.thread.id || action.thread.title) {
|
|
4210
|
+
lines.push(` thread: ${action.thread.title ?? "(untitled)"} ${action.thread.id ?? "(new)"}`);
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
if (plan.record_command) {
|
|
4214
|
+
lines.push(`Record after app tools: ${plan.record_command}`);
|
|
4215
|
+
}
|
|
4216
|
+
if (plan.receipt) {
|
|
4217
|
+
lines.push(`Receipt: ${plan.receipt.event_id}`);
|
|
4218
|
+
}
|
|
4219
|
+
return `${lines.join("\n")}\n`;
|
|
4220
|
+
}
|
|
3737
4221
|
function runAppAutopilotCommand(parsed, options) {
|
|
3738
4222
|
const action = parsed.flags.action;
|
|
3739
4223
|
if (action !== "start" && action !== "stop" && action !== "status") {
|
|
@@ -4049,6 +4533,7 @@ const QA_PLAN_SCENARIOS = new Set([
|
|
|
4049
4533
|
"tmux-errors",
|
|
4050
4534
|
"dispatch-completion",
|
|
4051
4535
|
"ralph-loop",
|
|
4536
|
+
"ship-it-loop",
|
|
4052
4537
|
"adversarial-triggers",
|
|
4053
4538
|
"goalbuddy-conveyor",
|
|
4054
4539
|
]);
|
|
@@ -4182,6 +4667,52 @@ function qaPlan(scenario) {
|
|
|
4182
4667
|
],
|
|
4183
4668
|
};
|
|
4184
4669
|
}
|
|
4670
|
+
if (scenario === "ship-it-loop") {
|
|
4671
|
+
return {
|
|
4672
|
+
authority_boundaries: [
|
|
4673
|
+
"Do not push a branch before repo.push_branch is permitted.",
|
|
4674
|
+
"Do not open or update a PR before repo.open_pr is permitted.",
|
|
4675
|
+
"Do not treat CI monitoring as CI truth; record explicit ci_green evidence.",
|
|
4676
|
+
"Do not resolve conflicts without a bounded manager instruction and retry limit.",
|
|
4677
|
+
"Do not merge before repo.merge_green_pr, ci_green, mergeability, manager_merge_decision, merge, post_merge_verification, and adversarial_check evidence exist.",
|
|
4678
|
+
],
|
|
4679
|
+
correlation_markers: [
|
|
4680
|
+
{ correlation_id: "ship-it-push-permission", purpose: "push branch permission gate" },
|
|
4681
|
+
{ correlation_id: "ship-it-open-pr-permission", purpose: "open PR permission gate" },
|
|
4682
|
+
{ correlation_id: "ship-it-merge-permission", purpose: "merge permission gate" },
|
|
4683
|
+
{ correlation_id: "ship-it-missing-evidence", purpose: "missing lifecycle evidence block" },
|
|
4684
|
+
{ correlation_id: "ship-it-conflict-block", purpose: "conflict retry limit proof" },
|
|
4685
|
+
{ correlation_id: "ship-it-allowed-closeout", purpose: "allowed closeout after all lifecycle evidence" },
|
|
4686
|
+
],
|
|
4687
|
+
evidence_template: {
|
|
4688
|
+
branch_ready: { branch: "<branch>", commit_sha: "<sha>" },
|
|
4689
|
+
branch_pushed: { remote: "origin", branch: "<branch>" },
|
|
4690
|
+
pr_url: { url: "<pull request URL>" },
|
|
4691
|
+
ci_green: { command: "gh pr checks --required", status: "green" },
|
|
4692
|
+
mergeability_clean: { conflicts: false, mergeable_state: "clean" },
|
|
4693
|
+
manager_merge_decision: { decision: "merge_ready", manager_verified: true },
|
|
4694
|
+
merge: { merge_sha: "<sha>" },
|
|
4695
|
+
post_merge_verification: { command: "<post-merge check>", status: "pass" },
|
|
4696
|
+
adversarial_check: { failure_mode: "<risk>", check: "<proof>", result: "<outcome>" },
|
|
4697
|
+
},
|
|
4698
|
+
expected_observations: [
|
|
4699
|
+
"push, PR creation, and merge commands fail closed until their manager permissions are granted",
|
|
4700
|
+
"missing lifecycle evidence blocks a continue_iteration before worker delivery",
|
|
4701
|
+
"unresolved conflicts are represented as bounded blockers, not hidden behind CI green",
|
|
4702
|
+
"a fresh retry delivers only after branch, PR, CI, mergeability, manager decision, merge, post-merge, and adversarial evidence exists",
|
|
4703
|
+
"the recipe and prompts keep merge readiness as a manager decision, not a worker claim",
|
|
4704
|
+
],
|
|
4705
|
+
scenario,
|
|
4706
|
+
steps: [
|
|
4707
|
+
"Create a disposable no-tmux task with the ship_it_loop template.",
|
|
4708
|
+
"Run the permission-gate checks for repo.push_branch, repo.open_pr, and repo.merge_green_pr.",
|
|
4709
|
+
"Attempt a lifecycle continuation before evidence and verify missing evidence blocks before worker delivery.",
|
|
4710
|
+
"Record partial PR/CI evidence and verify mergeability/manager-decision/merge/post-merge proof is still required.",
|
|
4711
|
+
"Record conflict retry-limit evidence as blocked when unresolved.",
|
|
4712
|
+
"Record all lifecycle receipts plus structured adversarial proof and verify a fresh retry reaches the worker inbox.",
|
|
4713
|
+
],
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4185
4716
|
if (scenario === "adversarial-triggers") {
|
|
4186
4717
|
return {
|
|
4187
4718
|
correlation_markers: [
|
|
@@ -4282,6 +4813,7 @@ const SUPPORTED_QA_RUN_SCENARIOS = new Set([
|
|
|
4282
4813
|
"generic-loop-template",
|
|
4283
4814
|
"generic-loop-template-browser",
|
|
4284
4815
|
"ralph-loop-guardrails",
|
|
4816
|
+
"ship-it-loop",
|
|
4285
4817
|
"test-coverage-loop",
|
|
4286
4818
|
]);
|
|
4287
4819
|
function isSupportedQaRunScenario(scenario) {
|
|
@@ -4324,6 +4856,9 @@ function runQaScenario(scenario, context) {
|
|
|
4324
4856
|
if (scenario === "build-clear-loop") {
|
|
4325
4857
|
return qaRunBuildClearLoop(context);
|
|
4326
4858
|
}
|
|
4859
|
+
if (scenario === "ship-it-loop") {
|
|
4860
|
+
return qaRunShipItLoop(context);
|
|
4861
|
+
}
|
|
4327
4862
|
if (scenario === "adversarial-triggers") {
|
|
4328
4863
|
return qaRunAdversarialTriggers(context);
|
|
4329
4864
|
}
|
|
@@ -4669,6 +5204,166 @@ function qaRunBuildClearLoop(context) {
|
|
|
4669
5204
|
template_metadata: templateMetadata,
|
|
4670
5205
|
};
|
|
4671
5206
|
}
|
|
5207
|
+
function qaRunShipItLoop(context) {
|
|
5208
|
+
const slug = randomUUID().slice(0, 8);
|
|
5209
|
+
const checks = [];
|
|
5210
|
+
const generatedTasks = [];
|
|
5211
|
+
const pushTask = createQaBoundTask(context, slug, "ship-it-push-permission");
|
|
5212
|
+
generatedTasks.push(generatedTask(pushTask, "ship-it-push-permission"));
|
|
5213
|
+
checks.push(qaRunPermissionGate(context, pushTask, {
|
|
5214
|
+
checkName: "ship_it_push_branch_requires_repo_push_branch",
|
|
5215
|
+
correlationId: "ship-it-push-permission-denied",
|
|
5216
|
+
message: "Push branch origin/codex/ship-it-loop.",
|
|
5217
|
+
permission: "repo.push_branch",
|
|
5218
|
+
}));
|
|
5219
|
+
qaConfigureManagerPermissions(context, pushTask, ["repo.push_branch"]);
|
|
5220
|
+
checks.push(qaRunPermissionGate(context, pushTask, {
|
|
5221
|
+
checkName: "ship_it_push_branch_delivers_after_permission",
|
|
5222
|
+
correlationId: "ship-it-push-permission-allowed",
|
|
5223
|
+
expectAllowed: true,
|
|
5224
|
+
message: "Push branch origin/codex/ship-it-loop after manager permission.",
|
|
5225
|
+
permission: "repo.push_branch",
|
|
5226
|
+
}));
|
|
5227
|
+
const prTask = createQaBoundTask(context, slug, "ship-it-open-pr-permission");
|
|
5228
|
+
generatedTasks.push(generatedTask(prTask, "ship-it-open-pr-permission"));
|
|
5229
|
+
checks.push(qaRunPermissionGate(context, prTask, {
|
|
5230
|
+
checkName: "ship_it_open_pr_requires_repo_open_pr",
|
|
5231
|
+
correlationId: "ship-it-open-pr-permission-denied",
|
|
5232
|
+
message: "Open PR for ship-it loop.",
|
|
5233
|
+
permission: "repo.open_pr",
|
|
5234
|
+
}));
|
|
5235
|
+
qaConfigureManagerPermissions(context, prTask, ["repo.open_pr"]);
|
|
5236
|
+
checks.push(qaRunPermissionGate(context, prTask, {
|
|
5237
|
+
checkName: "ship_it_open_pr_delivers_after_permission",
|
|
5238
|
+
correlationId: "ship-it-open-pr-permission-allowed",
|
|
5239
|
+
expectAllowed: true,
|
|
5240
|
+
message: "Open PR for ship-it loop after manager permission.",
|
|
5241
|
+
permission: "repo.open_pr",
|
|
5242
|
+
}));
|
|
5243
|
+
const mergeTask = createQaBoundTask(context, slug, "ship-it-merge-permission");
|
|
5244
|
+
generatedTasks.push(generatedTask(mergeTask, "ship-it-merge-permission"));
|
|
5245
|
+
checks.push(qaRunPermissionGate(context, mergeTask, {
|
|
5246
|
+
checkName: "ship_it_merge_requires_repo_merge_green_pr",
|
|
5247
|
+
correlationId: "ship-it-merge-permission-denied",
|
|
5248
|
+
message: "Merge PR after verified closeout.",
|
|
5249
|
+
permission: "repo.merge_green_pr",
|
|
5250
|
+
}));
|
|
5251
|
+
qaConfigureManagerPermissions(context, mergeTask, ["repo.merge_green_pr"]);
|
|
5252
|
+
checks.push(qaRunPermissionGate(context, mergeTask, {
|
|
5253
|
+
checkName: "ship_it_merge_delivers_after_permission",
|
|
5254
|
+
correlationId: "ship-it-merge-permission-allowed",
|
|
5255
|
+
expectAllowed: true,
|
|
5256
|
+
message: "Merge PR after verified closeout and manager permission.",
|
|
5257
|
+
permission: "repo.merge_green_pr",
|
|
5258
|
+
}));
|
|
5259
|
+
const lifecycleTask = createQaBoundTask(context, slug, "ship-it-lifecycle");
|
|
5260
|
+
generatedTasks.push(generatedTask(lifecycleTask, "ship-it-lifecycle"));
|
|
5261
|
+
const templateMetadata = loopTemplateMetadata("ship_it_loop", {
|
|
5262
|
+
currentIteration: 1,
|
|
5263
|
+
maxIterations: 2,
|
|
5264
|
+
seedPromptSha256: "qa-run-ship-it-seed",
|
|
5265
|
+
});
|
|
5266
|
+
const run = createQaRalphLoopRun(context, lifecycleTask, {
|
|
5267
|
+
currentIteration: 1,
|
|
5268
|
+
maxIterations: 2,
|
|
5269
|
+
metadata: templateMetadata,
|
|
5270
|
+
preset: "ship_it_loop",
|
|
5271
|
+
requiredBeforeContinue: asStringArray(templateMetadata.required_before_continue),
|
|
5272
|
+
seedPromptSha256: "qa-run-ship-it-seed",
|
|
5273
|
+
stopConditions: asStringArray(templateMetadata.stop_conditions),
|
|
5274
|
+
});
|
|
5275
|
+
enqueueQaContinue(context, lifecycleTask, run.id, "ship-it-missing-evidence", "Run ship-it continuation before lifecycle evidence.");
|
|
5276
|
+
const missing = qaDispatchContinueOnce(context, "ship-it-missing-evidence");
|
|
5277
|
+
const missingCounts = qaDeliveryCounts(context, lifecycleTask);
|
|
5278
|
+
qaExpectBlocked(missing, missingCounts, {
|
|
5279
|
+
message: "ship_it_loop missing lifecycle evidence",
|
|
5280
|
+
missingEvidence: asStringArray(templateMetadata.required_before_continue),
|
|
5281
|
+
reason: "missing_required_evidence",
|
|
5282
|
+
});
|
|
5283
|
+
checks.push(qaCheck("ship_it_lifecycle_blocks_before_any_evidence", missing, missingCounts));
|
|
5284
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "branch_ready", "ship-it-branch-ready", {
|
|
5285
|
+
metadata: { branch: "codex/ship-it-loop", commit_sha: "1111111111111111111111111111111111111111" },
|
|
5286
|
+
});
|
|
5287
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "branch_pushed", "ship-it-branch-pushed", {
|
|
5288
|
+
metadata: { branch: "codex/ship-it-loop", remote: "origin" },
|
|
5289
|
+
});
|
|
5290
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "pr_url", "ship-it-pr-url", {
|
|
5291
|
+
metadata: { url: "https://github.example.test/acme/repo/pull/42" },
|
|
5292
|
+
});
|
|
5293
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "ci_green", "ship-it-ci-green", {
|
|
5294
|
+
metadata: { command: "gh pr checks 42 --required", status: "green" },
|
|
5295
|
+
status: "green",
|
|
5296
|
+
});
|
|
5297
|
+
enqueueQaContinue(context, lifecycleTask, run.id, "ship-it-partial-evidence", "Run ship-it continuation after PR and CI but before merge readiness.");
|
|
5298
|
+
const partial = qaDispatchContinueOnce(context, "ship-it-partial-evidence");
|
|
5299
|
+
const partialCounts = qaDeliveryCounts(context, lifecycleTask);
|
|
5300
|
+
qaExpectBlocked(partial, partialCounts, {
|
|
5301
|
+
message: "ship_it_loop partial lifecycle evidence",
|
|
5302
|
+
missingEvidence: ["mergeability_clean", "manager_merge_decision", "merge", "post_merge_verification", "adversarial_check"],
|
|
5303
|
+
reason: "missing_required_evidence",
|
|
5304
|
+
});
|
|
5305
|
+
checks.push(qaCheck("ship_it_lifecycle_blocks_before_mergeability_and_manager_decision", partial, partialCounts));
|
|
5306
|
+
const artifactDir = qaArtifactDir(context, "ship-it-loop", slug, run.id);
|
|
5307
|
+
const conflictReceipt = join(artifactDir, "conflict-blocked.json");
|
|
5308
|
+
mkdirSync(dirname(conflictReceipt), { recursive: true });
|
|
5309
|
+
const conflictPayload = {
|
|
5310
|
+
conflict_state: "unresolved",
|
|
5311
|
+
max_retries: 2,
|
|
5312
|
+
retry_count: 2,
|
|
5313
|
+
status: "blocked",
|
|
5314
|
+
stop_reason: "conflict_retry_limit_reached",
|
|
5315
|
+
};
|
|
5316
|
+
writeFileSync(conflictReceipt, `${JSON.stringify(sortJson(conflictPayload), null, 2)}\n`);
|
|
5317
|
+
checks.push({
|
|
5318
|
+
artifact_path: conflictReceipt,
|
|
5319
|
+
conflict: conflictPayload,
|
|
5320
|
+
name: "ship_it_conflict_retry_blocks_after_limit",
|
|
5321
|
+
status: "passed",
|
|
5322
|
+
});
|
|
5323
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "mergeability_clean", "ship-it-mergeability-clean", {
|
|
5324
|
+
metadata: { conflicts: false, mergeable_state: "clean" },
|
|
5325
|
+
});
|
|
5326
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "manager_merge_decision", "ship-it-manager-merge-decision", {
|
|
5327
|
+
metadata: { decision: "merge_ready", manager_verified: true },
|
|
5328
|
+
});
|
|
5329
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "merge", "ship-it-merge", {
|
|
5330
|
+
metadata: { merge_sha: "2222222222222222222222222222222222222222" },
|
|
5331
|
+
});
|
|
5332
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "post_merge_verification", "ship-it-post-merge-verification", {
|
|
5333
|
+
metadata: { command: "git rev-parse HEAD && npm test -- --runInBand", status: "pass" },
|
|
5334
|
+
});
|
|
5335
|
+
qaRecordAdversarialEvidence(context, lifecycleTask, run.id, "ship-it-adversarial-proof", {
|
|
5336
|
+
check: "Inspect permission denials, missing-evidence blocks, conflict retry receipt, and final evidence set.",
|
|
5337
|
+
failure_mode: "A ship-it loop could merge after CI green while conflicts, manager decision, or post-merge proof are missing.",
|
|
5338
|
+
result: "Dispatch stayed blocked until mergeability, manager decision, merge, post-merge, and adversarial receipts were present.",
|
|
5339
|
+
});
|
|
5340
|
+
enqueueQaContinue(context, lifecycleTask, run.id, "ship-it-allowed-closeout", "Run ship-it continuation after all lifecycle evidence.");
|
|
5341
|
+
const allowed = qaDispatchContinueOnce(context, "ship-it-allowed-closeout");
|
|
5342
|
+
const allowedCounts = qaDeliveryCounts(context, lifecycleTask);
|
|
5343
|
+
qaExpectDelivered(allowed, allowedCounts, "ship_it_loop allowed closeout");
|
|
5344
|
+
checks.push(qaCheck("ship_it_lifecycle_retry_delivers_after_all_evidence", allowed, allowedCounts));
|
|
5345
|
+
return {
|
|
5346
|
+
artifacts: { conflict_receipt: conflictReceipt, db_path: context.dbPath },
|
|
5347
|
+
checks,
|
|
5348
|
+
generated_at: new Date().toISOString(),
|
|
5349
|
+
generated_tasks: generatedTasks,
|
|
5350
|
+
replay_commands: [
|
|
5351
|
+
"conveyor loop-templates --show ship_it_loop --json",
|
|
5352
|
+
"conveyor manager-recipes --show ship-it-loop --json",
|
|
5353
|
+
"conveyor manager-permission <task> repo.push_branch --require",
|
|
5354
|
+
"conveyor manager-permission <task> repo.open_pr --require",
|
|
5355
|
+
"conveyor manager-permission <task> repo.merge_green_pr --require",
|
|
5356
|
+
"conveyor loop-evidence add <task> --loop-run <run-id> --iteration 1 --evidence-type branch_ready",
|
|
5357
|
+
"conveyor loop-evidence add <task> --loop-run <run-id> --iteration 1 --evidence-type ci_green",
|
|
5358
|
+
"conveyor loop-evidence adversarial-check <task> --loop-run <run-id> --iteration 1 --failure-mode <failure> --check <check> --result <result>",
|
|
5359
|
+
`conveyor dispatch --once --type continue_iteration --dispatcher-id ${context.dispatcherId} --path ${context.dbPath}`,
|
|
5360
|
+
],
|
|
5361
|
+
result: "passed",
|
|
5362
|
+
scenario: "ship-it-loop",
|
|
5363
|
+
template: "ship_it_loop",
|
|
5364
|
+
template_metadata: templateMetadata,
|
|
5365
|
+
};
|
|
5366
|
+
}
|
|
4672
5367
|
function qaRunAdversarialTriggers(context) {
|
|
4673
5368
|
const slug = randomUUID().slice(0, 8);
|
|
4674
5369
|
const triggerDefinitions = listLoopTriggers();
|
|
@@ -4966,24 +5661,27 @@ function enqueueQaContinue(context, task, runId, correlationId, message) {
|
|
|
4966
5661
|
}
|
|
4967
5662
|
}
|
|
4968
5663
|
function qaDispatchContinueOnce(context, expectedCorrelationId) {
|
|
5664
|
+
return qaDispatchCommandOnce(context, "continue_iteration", expectedCorrelationId);
|
|
5665
|
+
}
|
|
5666
|
+
function qaDispatchCommandOnce(context, commandType, expectedCorrelationId) {
|
|
4969
5667
|
const before = openDatabaseSync(context.dbPath);
|
|
4970
5668
|
try {
|
|
4971
5669
|
initializeDatabaseSync(before);
|
|
4972
5670
|
const rows = before.prepare(`
|
|
4973
5671
|
select correlation_id, state
|
|
4974
5672
|
from commands
|
|
4975
|
-
where type =
|
|
5673
|
+
where type = ? and state in ('pending', 'attempted')
|
|
4976
5674
|
order by created_at, id
|
|
4977
|
-
`).all();
|
|
5675
|
+
`).all(commandType);
|
|
4978
5676
|
const seen = rows.map((row) => `${row.correlation_id}:${row.state}`);
|
|
4979
5677
|
if (rows.length !== 1 || rows[0]?.correlation_id !== expectedCorrelationId || rows[0]?.state !== "pending") {
|
|
4980
|
-
throw new Error(`qa-run
|
|
5678
|
+
throw new Error(`qa-run ${commandType} dispatch queue is not clean; expected only ${expectedCorrelationId}, found ${JSON.stringify(seen)}`);
|
|
4981
5679
|
}
|
|
4982
5680
|
}
|
|
4983
5681
|
finally {
|
|
4984
5682
|
before.close();
|
|
4985
5683
|
}
|
|
4986
|
-
const parsed = parseRuntimeArgs(["dispatch", "--type",
|
|
5684
|
+
const parsed = parseRuntimeArgs(["dispatch", "--type", commandType, "--path", context.dbPath], {
|
|
4987
5685
|
AGENT_CONVEYOR_TS_RUNTIME: "1",
|
|
4988
5686
|
});
|
|
4989
5687
|
const processed = dispatchOncePass(parsed, context.runtimeOptions, {
|
|
@@ -4993,13 +5691,68 @@ function qaDispatchContinueOnce(context, expectedCorrelationId) {
|
|
|
4993
5691
|
limit: 1,
|
|
4994
5692
|
});
|
|
4995
5693
|
if (processed.length !== 1) {
|
|
4996
|
-
throw new Error(`expected exactly one
|
|
5694
|
+
throw new Error(`expected exactly one ${commandType} dispatch item, got ${processed.length}`);
|
|
4997
5695
|
}
|
|
4998
5696
|
if (processed[0]?.correlation_id !== expectedCorrelationId) {
|
|
4999
5697
|
throw new Error(`qa-run dispatched unexpected command ${String(processed[0]?.correlation_id)}`);
|
|
5000
5698
|
}
|
|
5001
5699
|
return processed[0] ?? {};
|
|
5002
5700
|
}
|
|
5701
|
+
function qaConfigureManagerPermissions(context, task, permissions) {
|
|
5702
|
+
const result = runTypescriptRuntimeCommand({
|
|
5703
|
+
...context.runtimeOptions,
|
|
5704
|
+
args: [
|
|
5705
|
+
"manager-config",
|
|
5706
|
+
task.task_name,
|
|
5707
|
+
"--mode",
|
|
5708
|
+
"strict",
|
|
5709
|
+
"--objective",
|
|
5710
|
+
"Ship-it lifecycle QA permission contract.",
|
|
5711
|
+
...permissions.flatMap((permission) => ["--permit", permission]),
|
|
5712
|
+
"--path",
|
|
5713
|
+
context.dbPath,
|
|
5714
|
+
],
|
|
5715
|
+
env: {
|
|
5716
|
+
...(context.runtimeOptions.env ?? {}),
|
|
5717
|
+
AGENT_CONVEYOR_TS_RUNTIME: "1",
|
|
5718
|
+
},
|
|
5719
|
+
});
|
|
5720
|
+
qaRequire(result.exitCode === 0, `manager-config permission setup failed: ${result.stderr ?? result.stdout ?? ""}`);
|
|
5721
|
+
}
|
|
5722
|
+
function qaRunPermissionGate(context, task, options) {
|
|
5723
|
+
const database = openDatabaseSync(context.dbPath);
|
|
5724
|
+
try {
|
|
5725
|
+
initializeDatabaseSync(database);
|
|
5726
|
+
createCommandSync(database, {
|
|
5727
|
+
commandType: "nudge_worker",
|
|
5728
|
+
correlationId: options.correlationId,
|
|
5729
|
+
payload: { message: options.message, ship_it: { required_permission: options.permission } },
|
|
5730
|
+
requiredPermission: options.permission,
|
|
5731
|
+
taskId: task.task_id,
|
|
5732
|
+
});
|
|
5733
|
+
}
|
|
5734
|
+
finally {
|
|
5735
|
+
database.close();
|
|
5736
|
+
}
|
|
5737
|
+
const dispatch = qaDispatchCommandOnce(context, "nudge_worker", options.correlationId);
|
|
5738
|
+
const counts = qaDeliveryCounts(context, task);
|
|
5739
|
+
if (options.expectAllowed === true) {
|
|
5740
|
+
qaExpectDelivered(dispatch, counts, `${options.permission} permission gate`);
|
|
5741
|
+
}
|
|
5742
|
+
else {
|
|
5743
|
+
qaRequire(dispatch.state === "failed", `${options.permission} gate did not fail without permission`);
|
|
5744
|
+
qaRequire(String(dispatch.error ?? "").includes("manager permission required"), `${options.permission} gate failed for the wrong reason`);
|
|
5745
|
+
qaRequire(counts.worker_inbox_count === 0, `${options.permission} denied gate left worker inbox mail`);
|
|
5746
|
+
}
|
|
5747
|
+
return {
|
|
5748
|
+
...counts,
|
|
5749
|
+
command_type: "nudge_worker",
|
|
5750
|
+
dispatch,
|
|
5751
|
+
name: options.checkName,
|
|
5752
|
+
permission: options.permission,
|
|
5753
|
+
status: "passed",
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5003
5756
|
function qaDeliveryCounts(context, task) {
|
|
5004
5757
|
const database = openDatabaseSync(context.dbPath);
|
|
5005
5758
|
try {
|
|
@@ -5309,6 +6062,269 @@ function runTasksCommand(parsed, options) {
|
|
|
5309
6062
|
database.close();
|
|
5310
6063
|
}
|
|
5311
6064
|
}
|
|
6065
|
+
function runCampaignCommand(parsed, options) {
|
|
6066
|
+
if (parsed.task) {
|
|
6067
|
+
return errorResult(`Unexpected argument: ${parsed.task}`);
|
|
6068
|
+
}
|
|
6069
|
+
const action = parsed.flags.action;
|
|
6070
|
+
if (!action) {
|
|
6071
|
+
return errorResult("campaign requires an action: create, add-slot, brief, assign, asset, or status");
|
|
6072
|
+
}
|
|
6073
|
+
const campaign = campaignNameArg(parsed);
|
|
6074
|
+
const database = openRuntimeDatabase(parsed, options);
|
|
6075
|
+
try {
|
|
6076
|
+
if (action === "create") {
|
|
6077
|
+
const objective = requiredStringFlag(parsed.flags.objective, "--objective");
|
|
6078
|
+
const metadata = jsonObjectArg(parsed.flags.metadataJson, "--metadata-json");
|
|
6079
|
+
const campaignId = createCampaignSync(database, {
|
|
6080
|
+
metadata,
|
|
6081
|
+
name: campaign,
|
|
6082
|
+
objective,
|
|
6083
|
+
});
|
|
6084
|
+
return campaignResult(parsed, {
|
|
6085
|
+
action,
|
|
6086
|
+
campaign,
|
|
6087
|
+
campaign_id: campaignId,
|
|
6088
|
+
created: true,
|
|
6089
|
+
}, [`campaign ${campaign} created ${campaignId}`]);
|
|
6090
|
+
}
|
|
6091
|
+
if (action === "add-slot") {
|
|
6092
|
+
const slotKey = requiredStringFlag(parsed.flags.slotKey, "--slot-key");
|
|
6093
|
+
const roleLabel = requiredStringFlag(parsed.flags.roleLabel, "--role-label");
|
|
6094
|
+
const state = campaignSlotStateArg(parsed.flags.statusState);
|
|
6095
|
+
const metadata = jsonObjectArg(parsed.flags.metadataJson, "--metadata-json");
|
|
6096
|
+
const slotId = addCampaignWorkerSlotSync(database, {
|
|
6097
|
+
campaign,
|
|
6098
|
+
channel: parsed.flags.channel,
|
|
6099
|
+
codexAppThreadId: parsed.flags.threadId,
|
|
6100
|
+
codexAppThreadTitle: parsed.flags.threadTitle,
|
|
6101
|
+
metadata,
|
|
6102
|
+
roleLabel,
|
|
6103
|
+
sessionId: parsed.flags.sessionId,
|
|
6104
|
+
slotKey,
|
|
6105
|
+
...(state ? { state } : {}),
|
|
6106
|
+
});
|
|
6107
|
+
return campaignResult(parsed, {
|
|
6108
|
+
action,
|
|
6109
|
+
campaign,
|
|
6110
|
+
created: true,
|
|
6111
|
+
slot_id: slotId,
|
|
6112
|
+
slot_key: slotKey,
|
|
6113
|
+
}, [`campaign ${campaign} slot ${slotKey} created ${slotId}`]);
|
|
6114
|
+
}
|
|
6115
|
+
if (action === "attach-slot") {
|
|
6116
|
+
const slot = requiredStringFlag(parsed.flags.slot, "--slot");
|
|
6117
|
+
const state = campaignSlotStateArg(parsed.flags.statusState) ?? "active";
|
|
6118
|
+
const record = updateCampaignWorkerSlotLifecycleSync(database, {
|
|
6119
|
+
campaign,
|
|
6120
|
+
codexAppThreadId: parsed.flags.threadId,
|
|
6121
|
+
codexAppThreadTitle: parsed.flags.threadTitle,
|
|
6122
|
+
metadata: jsonObjectArg(parsed.flags.metadataJson, "--metadata-json"),
|
|
6123
|
+
sessionId: parsed.flags.sessionId,
|
|
6124
|
+
slot,
|
|
6125
|
+
state,
|
|
6126
|
+
});
|
|
6127
|
+
return campaignResult(parsed, {
|
|
6128
|
+
action,
|
|
6129
|
+
campaign,
|
|
6130
|
+
slot: record,
|
|
6131
|
+
updated: true,
|
|
6132
|
+
}, [`campaign ${campaign} slot ${record.slot_key} attached ${record.id}`]);
|
|
6133
|
+
}
|
|
6134
|
+
if (action === "rotate-slot") {
|
|
6135
|
+
const slot = requiredStringFlag(parsed.flags.slot, "--slot");
|
|
6136
|
+
const expectedThreadId = requiredStringFlag(parsed.flags.expectedThreadId, "--expected-thread-id");
|
|
6137
|
+
const nextThreadId = requiredStringFlag(parsed.flags.threadId, "--thread-id");
|
|
6138
|
+
const record = updateCampaignWorkerSlotLifecycleSync(database, {
|
|
6139
|
+
campaign,
|
|
6140
|
+
codexAppThreadId: nextThreadId,
|
|
6141
|
+
codexAppThreadTitle: parsed.flags.threadTitle,
|
|
6142
|
+
expectedThreadId,
|
|
6143
|
+
sessionId: parsed.flags.sessionId,
|
|
6144
|
+
slot,
|
|
6145
|
+
state: campaignSlotStateArg(parsed.flags.statusState) ?? "active",
|
|
6146
|
+
});
|
|
6147
|
+
return campaignResult(parsed, {
|
|
6148
|
+
action,
|
|
6149
|
+
campaign,
|
|
6150
|
+
expected_thread_id: expectedThreadId,
|
|
6151
|
+
slot: record,
|
|
6152
|
+
updated: true,
|
|
6153
|
+
}, [`campaign ${campaign} slot ${record.slot_key} rotated ${expectedThreadId} -> ${record.codex_app_thread_id ?? "none"}`]);
|
|
6154
|
+
}
|
|
6155
|
+
if (action === "archive-slot") {
|
|
6156
|
+
const slot = requiredStringFlag(parsed.flags.slot, "--slot");
|
|
6157
|
+
const expectedThreadId = requiredStringFlag(parsed.flags.expectedThreadId, "--expected-thread-id");
|
|
6158
|
+
const record = updateCampaignWorkerSlotLifecycleSync(database, {
|
|
6159
|
+
campaign,
|
|
6160
|
+
expectedThreadId,
|
|
6161
|
+
slot,
|
|
6162
|
+
state: "archived",
|
|
6163
|
+
});
|
|
6164
|
+
return campaignResult(parsed, {
|
|
6165
|
+
action,
|
|
6166
|
+
campaign,
|
|
6167
|
+
expected_thread_id: expectedThreadId,
|
|
6168
|
+
slot: record,
|
|
6169
|
+
updated: true,
|
|
6170
|
+
}, [`campaign ${campaign} slot ${record.slot_key} archived ${record.id}`]);
|
|
6171
|
+
}
|
|
6172
|
+
if (action === "brief") {
|
|
6173
|
+
const channel = requiredStringFlag(parsed.flags.channel, "--channel");
|
|
6174
|
+
if (parsed.flags.briefJson === null) {
|
|
6175
|
+
return errorResult("campaign brief requires --brief-json");
|
|
6176
|
+
}
|
|
6177
|
+
const briefId = upsertCampaignChannelBriefSync(database, {
|
|
6178
|
+
brief: jsonObjectArg(parsed.flags.briefJson, "--brief-json"),
|
|
6179
|
+
campaign,
|
|
6180
|
+
channel,
|
|
6181
|
+
});
|
|
6182
|
+
return campaignResult(parsed, {
|
|
6183
|
+
action,
|
|
6184
|
+
brief_id: briefId,
|
|
6185
|
+
campaign,
|
|
6186
|
+
channel,
|
|
6187
|
+
upserted: true,
|
|
6188
|
+
}, [`campaign ${campaign} brief ${channel} upserted ${briefId}`]);
|
|
6189
|
+
}
|
|
6190
|
+
if (action === "assign") {
|
|
6191
|
+
const slot = requiredStringFlag(parsed.flags.slot, "--slot");
|
|
6192
|
+
const title = requiredStringFlag(parsed.flags.title, "--title");
|
|
6193
|
+
const instructions = requiredStringFlag(parsed.flags.instructions, "--instructions");
|
|
6194
|
+
const status = campaignAssignmentStatusArg(parsed.flags.statusState);
|
|
6195
|
+
const metadata = jsonObjectArg(parsed.flags.metadataJson, "--metadata-json");
|
|
6196
|
+
const assignmentId = createCampaignAssignmentSync(database, {
|
|
6197
|
+
campaign,
|
|
6198
|
+
instructions,
|
|
6199
|
+
metadata,
|
|
6200
|
+
slot,
|
|
6201
|
+
title,
|
|
6202
|
+
...(status ? { status } : {}),
|
|
6203
|
+
});
|
|
6204
|
+
return campaignResult(parsed, {
|
|
6205
|
+
action,
|
|
6206
|
+
assignment_id: assignmentId,
|
|
6207
|
+
campaign,
|
|
6208
|
+
created: true,
|
|
6209
|
+
slot_id: slot,
|
|
6210
|
+
}, [`campaign ${campaign} assignment created ${assignmentId}`]);
|
|
6211
|
+
}
|
|
6212
|
+
if (action === "asset") {
|
|
6213
|
+
const slot = requiredStringFlag(parsed.flags.slot, "--slot");
|
|
6214
|
+
const title = requiredStringFlag(parsed.flags.title, "--title");
|
|
6215
|
+
const assetType = campaignAssetTypeArg(requiredStringFlag(parsed.flags.assetType, "--asset-type"));
|
|
6216
|
+
const status = campaignAssetStatusArg(parsed.flags.statusState);
|
|
6217
|
+
const metadata = jsonObjectArg(parsed.flags.metadataJson, "--metadata-json");
|
|
6218
|
+
const assetReceiptId = recordCampaignAssetReceiptSync(database, {
|
|
6219
|
+
artifactPath: parsed.flags.artifactPath,
|
|
6220
|
+
assetType,
|
|
6221
|
+
assignment: parsed.flags.assignment,
|
|
6222
|
+
campaign,
|
|
6223
|
+
channel: parsed.flags.channel,
|
|
6224
|
+
metadata,
|
|
6225
|
+
promptSummary: parsed.flags.promptSummary,
|
|
6226
|
+
reviewNotes: parsed.flags.reviewNotes,
|
|
6227
|
+
slot,
|
|
6228
|
+
title,
|
|
6229
|
+
...(status ? { status } : {}),
|
|
6230
|
+
});
|
|
6231
|
+
return campaignResult(parsed, {
|
|
6232
|
+
action,
|
|
6233
|
+
asset_receipt_id: assetReceiptId,
|
|
6234
|
+
campaign,
|
|
6235
|
+
created: true,
|
|
6236
|
+
slot_id: slot,
|
|
6237
|
+
}, [`campaign ${campaign} asset receipt created ${assetReceiptId}`]);
|
|
6238
|
+
}
|
|
6239
|
+
if (action === "status") {
|
|
6240
|
+
const status = campaignStatusSync(database, campaign);
|
|
6241
|
+
return campaignResult(parsed, status, renderCampaignStatusText(status));
|
|
6242
|
+
}
|
|
6243
|
+
if (action === "dashboard") {
|
|
6244
|
+
const dashboard = campaignDashboardSync(database, campaign);
|
|
6245
|
+
return campaignResult(parsed, dashboard, renderCampaignDashboardText(dashboard));
|
|
6246
|
+
}
|
|
6247
|
+
return errorResult(`Unsupported campaign action: ${action}`);
|
|
6248
|
+
}
|
|
6249
|
+
finally {
|
|
6250
|
+
database.close();
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
function campaignResult(parsed, payload, lines) {
|
|
6254
|
+
return parsed.flags.json ? jsonResult(payload) : textResult(lines);
|
|
6255
|
+
}
|
|
6256
|
+
function campaignNameArg(parsed) {
|
|
6257
|
+
if (parsed.flags.names.length !== 1) {
|
|
6258
|
+
throw new Error("campaign requires exactly one --name <campaign>");
|
|
6259
|
+
}
|
|
6260
|
+
return parsed.flags.names[0];
|
|
6261
|
+
}
|
|
6262
|
+
function requiredStringFlag(value, flag) {
|
|
6263
|
+
if (value === null || value.length === 0) {
|
|
6264
|
+
throw new Error(`campaign requires ${flag}`);
|
|
6265
|
+
}
|
|
6266
|
+
return value;
|
|
6267
|
+
}
|
|
6268
|
+
function campaignSlotStateArg(value) {
|
|
6269
|
+
if (value === null) {
|
|
6270
|
+
return undefined;
|
|
6271
|
+
}
|
|
6272
|
+
if (value === "active" || value === "archived" || value === "blocked" || value === "idle" || value === "planned") {
|
|
6273
|
+
return value;
|
|
6274
|
+
}
|
|
6275
|
+
throw new Error("--state must be one of: active, archived, blocked, idle, planned");
|
|
6276
|
+
}
|
|
6277
|
+
function campaignAssignmentStatusArg(value) {
|
|
6278
|
+
if (value === null) {
|
|
6279
|
+
return undefined;
|
|
6280
|
+
}
|
|
6281
|
+
if (value === "active" || value === "blocked" || value === "cancelled" || value === "done" || value === "queued") {
|
|
6282
|
+
return value;
|
|
6283
|
+
}
|
|
6284
|
+
throw new Error("--status must be one of: active, blocked, cancelled, done, queued");
|
|
6285
|
+
}
|
|
6286
|
+
function campaignAssetStatusArg(value) {
|
|
6287
|
+
if (value === null) {
|
|
6288
|
+
return undefined;
|
|
6289
|
+
}
|
|
6290
|
+
if (value === "approved" || value === "draft" || value === "needs_review" || value === "published" || value === "rejected") {
|
|
6291
|
+
return value;
|
|
6292
|
+
}
|
|
6293
|
+
throw new Error("--status must be one of: approved, draft, needs_review, published, rejected");
|
|
6294
|
+
}
|
|
6295
|
+
function campaignAssetTypeArg(value) {
|
|
6296
|
+
if (value === "audio" || value === "copy" || value === "hyperframes" || value === "image" || value === "other" || value === "video") {
|
|
6297
|
+
return value;
|
|
6298
|
+
}
|
|
6299
|
+
throw new Error("--asset-type must be one of: audio, copy, hyperframes, image, other, video");
|
|
6300
|
+
}
|
|
6301
|
+
function renderCampaignStatusText(status) {
|
|
6302
|
+
return [
|
|
6303
|
+
`campaign ${status.campaign.name} ${status.campaign.status}`,
|
|
6304
|
+
`slots ${status.slots.length}`,
|
|
6305
|
+
`assignments ${statusCountsText(status.assignment_counts)}`,
|
|
6306
|
+
`assets ${statusCountsText(status.asset_counts)}`,
|
|
6307
|
+
`briefs ${status.channel_briefs.length}`,
|
|
6308
|
+
];
|
|
6309
|
+
}
|
|
6310
|
+
function renderCampaignDashboardText(dashboard) {
|
|
6311
|
+
const lines = [
|
|
6312
|
+
`campaign ${dashboard.campaign.name} ${dashboard.campaign.status}`,
|
|
6313
|
+
`next ${dashboard.next_manager_action.action}: ${dashboard.next_manager_action.reason}`,
|
|
6314
|
+
`workers active=${dashboard.summary.active_slots} stale=${dashboard.summary.stale_slots} blocked=${dashboard.summary.blocked_slots} archived=${dashboard.summary.archived_slots}`,
|
|
6315
|
+
`assignments ${statusCountsText(dashboard.assignment_counts)}`,
|
|
6316
|
+
`assets ${statusCountsText(dashboard.asset_counts)}`,
|
|
6317
|
+
`approvals needs_review=${dashboard.approvals.needs_review} approved=${dashboard.approvals.approved} rejected=${dashboard.approvals.rejected} published=${dashboard.approvals.published}`,
|
|
6318
|
+
`blockers ${dashboard.blockers.length}`,
|
|
6319
|
+
];
|
|
6320
|
+
for (const slot of dashboard.slots.slice(0, 8)) {
|
|
6321
|
+
lines.push(`slot ${slot.slot_key} ${slot.state} ${slot.lifecycle.state} assignments=${slot.assignments.length} assets=${slot.assets.length}`);
|
|
6322
|
+
}
|
|
6323
|
+
return lines;
|
|
6324
|
+
}
|
|
6325
|
+
function statusCountsText(counts) {
|
|
6326
|
+
return Object.entries(counts).map(([status, count]) => `${status}=${count}`).join(" ");
|
|
6327
|
+
}
|
|
5312
6328
|
function runLegacyStartCommand(parsed, options) {
|
|
5313
6329
|
if (!parsed.task) {
|
|
5314
6330
|
return errorResult("start requires a session.");
|
|
@@ -5503,7 +6519,14 @@ function runInstallSkillsCommand(parsed, options) {
|
|
|
5503
6519
|
return { exitCode: 0, handled: true, stdout: `${lines.join("\n")}\n` };
|
|
5504
6520
|
}
|
|
5505
6521
|
function dashboardLaunchPayload(parsed) {
|
|
5506
|
-
const
|
|
6522
|
+
const queryParams = new URLSearchParams();
|
|
6523
|
+
if (parsed.flags.taskName) {
|
|
6524
|
+
queryParams.set("task", parsed.flags.taskName);
|
|
6525
|
+
}
|
|
6526
|
+
if (parsed.flags.campaignName) {
|
|
6527
|
+
queryParams.set("campaign", parsed.flags.campaignName);
|
|
6528
|
+
}
|
|
6529
|
+
const query = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
5507
6530
|
const command = [
|
|
5508
6531
|
"npm",
|
|
5509
6532
|
"run",
|
|
@@ -5519,6 +6542,9 @@ function dashboardLaunchPayload(parsed) {
|
|
|
5519
6542
|
if (parsed.flags.taskName) {
|
|
5520
6543
|
command.push("--task", parsed.flags.taskName);
|
|
5521
6544
|
}
|
|
6545
|
+
if (parsed.flags.campaignName) {
|
|
6546
|
+
command.push("--campaign", parsed.flags.campaignName);
|
|
6547
|
+
}
|
|
5522
6548
|
if (parsed.flags.path) {
|
|
5523
6549
|
command.push("--db-path", parsed.flags.path);
|
|
5524
6550
|
}
|
|
@@ -5529,6 +6555,7 @@ function dashboardLaunchPayload(parsed) {
|
|
|
5529
6555
|
ensure_dispatch: parsed.flags.require,
|
|
5530
6556
|
host: parsed.flags.host,
|
|
5531
6557
|
port: parsed.flags.port,
|
|
6558
|
+
campaign: parsed.flags.campaignName,
|
|
5532
6559
|
task: parsed.flags.taskName,
|
|
5533
6560
|
url: `http://${parsed.flags.host}:${parsed.flags.port}/${query}`,
|
|
5534
6561
|
};
|
|
@@ -11942,8 +12969,10 @@ const MANAGER_PERMISSION_ACTION_NAMES = new Set([
|
|
|
11942
12969
|
"context.fetch_prs",
|
|
11943
12970
|
"context.spawn_reviewer",
|
|
11944
12971
|
"repo.merge_green_pr",
|
|
12972
|
+
"repo.monitor_ci",
|
|
11945
12973
|
"repo.open_pr",
|
|
11946
12974
|
"repo.push_branch",
|
|
12975
|
+
"repo.resolve_conflicts",
|
|
11947
12976
|
"verification.run_cargo",
|
|
11948
12977
|
"verification.run_playwright",
|
|
11949
12978
|
"verification.run_pytest",
|
|
@@ -14100,6 +15129,7 @@ const DEFERRED_HEADING_RE = /\b(follow[- ]?up|deferred)\b/i;
|
|
|
14100
15129
|
const LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)]|\[[ xX]\])\s+(?<text>.+?)\s*$/;
|
|
14101
15130
|
const EMPTY_ITEM_RE = /^(?:n\/?a|none|no follow[- ]?ups?|no deferred(?: criteria)?|nothing)$/i;
|
|
14102
15131
|
const INDENTED_CONTINUATION_RE = /^\s+\S/;
|
|
15132
|
+
const CLOSEOUT_CRITERION_RE = /\b(?:finish-task|require-criteria-audit|task (?:is )?(?:marked )?done|mark(?:ed)? (?:the )?task done|terminal closeout|verified task closeout|heartbeat teardown|final manager (?:report|decision)|manager final (?:report|handoff)|closeout proof|control-plane closeout)\b/i;
|
|
14103
15133
|
function planCriteriaCommands(task, text, options) {
|
|
14104
15134
|
const { suggestions, warnings } = parseWorkerCriteriaResponse(text);
|
|
14105
15135
|
return {
|
|
@@ -14161,6 +15191,11 @@ function parseWorkerCriteriaResponse(text) {
|
|
|
14161
15191
|
else if (suggestions.length === 0) {
|
|
14162
15192
|
warnings.push("Clear criteria headings were found, but no bullet or numbered criteria items were detected.");
|
|
14163
15193
|
}
|
|
15194
|
+
for (const suggestion of suggestions) {
|
|
15195
|
+
if (suggestion.classification?.kind === "manager_closeout_proof") {
|
|
15196
|
+
warnings.push(`Criterion "${suggestion.criterion}" appears to describe manager closeout/control-plane proof. Keep closeout proof in the manager final report, audit, replay, or epilogue evidence instead of accepted worker/task criteria unless this task is explicitly Conveyor closeout QA.`);
|
|
15197
|
+
}
|
|
15198
|
+
}
|
|
14164
15199
|
return { suggestions, warnings };
|
|
14165
15200
|
}
|
|
14166
15201
|
function headingStatus(line) {
|
|
@@ -14182,12 +15217,23 @@ function makeCriteriaSuggestion(text, status) {
|
|
|
14182
15217
|
return null;
|
|
14183
15218
|
}
|
|
14184
15219
|
return {
|
|
15220
|
+
classification: classifyCriteriaSuggestion(criterion),
|
|
14185
15221
|
criterion,
|
|
14186
15222
|
rationale: status === "deferred" ? DEFAULT_DEFERRED_RATIONALE : null,
|
|
14187
15223
|
source: "worker_proposed",
|
|
14188
15224
|
status,
|
|
14189
15225
|
};
|
|
14190
15226
|
}
|
|
15227
|
+
function classifyCriteriaSuggestion(criterion) {
|
|
15228
|
+
if (!CLOSEOUT_CRITERION_RE.test(criterion)) {
|
|
15229
|
+
return null;
|
|
15230
|
+
}
|
|
15231
|
+
return {
|
|
15232
|
+
kind: "manager_closeout_proof",
|
|
15233
|
+
reason: "The criterion names manager closeout mechanics rather than the worker/task outcome being accepted.",
|
|
15234
|
+
recommendation: "keep_out_of_acceptance_criteria",
|
|
15235
|
+
};
|
|
15236
|
+
}
|
|
14191
15237
|
function suggestionToArgv(task, suggestion, options) {
|
|
14192
15238
|
const argv = [
|
|
14193
15239
|
"conveyor",
|
|
@@ -14257,6 +15303,8 @@ function isDefaultRuntimeCommand(command) {
|
|
|
14257
15303
|
|| command === "app-wakeup-plan"
|
|
14258
15304
|
|| command === "app-wakeup-dispatch"
|
|
14259
15305
|
|| command === "app-wakeup-record-delivery"
|
|
15306
|
+
|| command === "app-worker-rotation-plan"
|
|
15307
|
+
|| command === "app-worker-rotation-record"
|
|
14260
15308
|
|| command === "app-autopilot"
|
|
14261
15309
|
|| command === "loop-templates"
|
|
14262
15310
|
|| command === "loop-triggers"
|
|
@@ -14272,6 +15320,7 @@ function isDefaultRuntimeCommand(command) {
|
|
|
14272
15320
|
|| command === "replay"
|
|
14273
15321
|
|| command === "export-task"
|
|
14274
15322
|
|| command === "tasks"
|
|
15323
|
+
|| command === "campaign"
|
|
14275
15324
|
|| command === "bind"
|
|
14276
15325
|
|| command === "unbind"
|
|
14277
15326
|
|| command === "create-disposable-binding"
|
|
@@ -14723,7 +15772,6 @@ function unsupportedLifecycleTaskOptions(parsed, finish) {
|
|
|
14723
15772
|
|| parsed.flags.includeFullTranscripts
|
|
14724
15773
|
|| parsed.flags.includeLegacy
|
|
14725
15774
|
|| parsed.flags.includeTranscripts
|
|
14726
|
-
|| parsed.flags.json
|
|
14727
15775
|
|| parsed.flags.limit !== null
|
|
14728
15776
|
|| parsed.flags.names.length > 0
|
|
14729
15777
|
|| parsed.flags.output !== null
|
|
@@ -15879,7 +16927,91 @@ const ADVERSARIAL_CHECK_REQUIREMENT = {
|
|
|
15879
16927
|
required: ["failure_mode", "check", "result"],
|
|
15880
16928
|
type: "object",
|
|
15881
16929
|
};
|
|
16930
|
+
const SHIP_IT_ARTIFACT_REQUIREMENTS = {
|
|
16931
|
+
adversarial_check: ADVERSARIAL_CHECK_REQUIREMENT,
|
|
16932
|
+
branch_pushed: {
|
|
16933
|
+
description: "Receipt that the worker branch was pushed only after repo.push_branch was permitted.",
|
|
16934
|
+
properties: {
|
|
16935
|
+
branch: { type: "string" },
|
|
16936
|
+
remote: { type: "string" },
|
|
16937
|
+
},
|
|
16938
|
+
required: ["branch", "remote"],
|
|
16939
|
+
type: "object",
|
|
16940
|
+
},
|
|
16941
|
+
branch_ready: {
|
|
16942
|
+
description: "Branch and commit evidence for the candidate ship-it change.",
|
|
16943
|
+
properties: {
|
|
16944
|
+
branch: { type: "string" },
|
|
16945
|
+
commit_sha: { type: "string" },
|
|
16946
|
+
},
|
|
16947
|
+
required: ["branch", "commit_sha"],
|
|
16948
|
+
type: "object",
|
|
16949
|
+
},
|
|
16950
|
+
ci_green: {
|
|
16951
|
+
description: "Explicit CI/check evidence. Prefer gh pr checks --required, or record why no required checks exist.",
|
|
16952
|
+
properties: {
|
|
16953
|
+
command: { type: "string" },
|
|
16954
|
+
status: { type: "string" },
|
|
16955
|
+
},
|
|
16956
|
+
required: ["command", "status"],
|
|
16957
|
+
type: "object",
|
|
16958
|
+
},
|
|
16959
|
+
manager_merge_decision: {
|
|
16960
|
+
description: "Manager-owned decision that all required evidence has been independently verified and merge is allowed.",
|
|
16961
|
+
properties: {
|
|
16962
|
+
decision: { type: "string" },
|
|
16963
|
+
manager_verified: { type: "boolean" },
|
|
16964
|
+
},
|
|
16965
|
+
required: ["decision", "manager_verified"],
|
|
16966
|
+
type: "object",
|
|
16967
|
+
},
|
|
16968
|
+
merge: {
|
|
16969
|
+
description: "Merge receipt recorded only after repo.merge_green_pr, CI, mergeability, and manager decision gates pass.",
|
|
16970
|
+
properties: {
|
|
16971
|
+
merge_sha: { type: "string" },
|
|
16972
|
+
},
|
|
16973
|
+
required: ["merge_sha"],
|
|
16974
|
+
type: "object",
|
|
16975
|
+
},
|
|
16976
|
+
mergeability_clean: {
|
|
16977
|
+
description: "Evidence that the PR is mergeable or conflicts were resolved within the manager-approved retry limit.",
|
|
16978
|
+
properties: {
|
|
16979
|
+
conflicts: { type: "boolean" },
|
|
16980
|
+
mergeable_state: { type: "string" },
|
|
16981
|
+
},
|
|
16982
|
+
required: ["conflicts", "mergeable_state"],
|
|
16983
|
+
type: "object",
|
|
16984
|
+
},
|
|
16985
|
+
post_merge_verification: {
|
|
16986
|
+
description: "Post-merge or main-branch verification receipt.",
|
|
16987
|
+
properties: {
|
|
16988
|
+
command: { type: "string" },
|
|
16989
|
+
status: { type: "string" },
|
|
16990
|
+
},
|
|
16991
|
+
required: ["command", "status"],
|
|
16992
|
+
type: "object",
|
|
16993
|
+
},
|
|
16994
|
+
pr_url: {
|
|
16995
|
+
description: "Pull request URL recorded only after repo.open_pr was permitted.",
|
|
16996
|
+
properties: {
|
|
16997
|
+
url: { type: "string" },
|
|
16998
|
+
},
|
|
16999
|
+
required: ["url"],
|
|
17000
|
+
type: "object",
|
|
17001
|
+
},
|
|
17002
|
+
};
|
|
15882
17003
|
const LOOP_TEMPLATES = {
|
|
17004
|
+
app_visible_build_loop: {
|
|
17005
|
+
artifactRequirements: { adversarial_check: ADVERSARIAL_CHECK_REQUIREMENT },
|
|
17006
|
+
cleanupPolicy: "off",
|
|
17007
|
+
description: "Require build evidence and adversarial proof between visible Codex app iterations without a cleanup gate.",
|
|
17008
|
+
maxIterations: 2,
|
|
17009
|
+
name: "app_visible_build_loop",
|
|
17010
|
+
recommendedTools: ["verification.run_tests"],
|
|
17011
|
+
requiredBeforeContinue: ["build_passed", "adversarial_check"],
|
|
17012
|
+
stopConditions: ["max_iterations", "required_evidence"],
|
|
17013
|
+
tags: ["build", "codex_app", "visible_session"],
|
|
17014
|
+
},
|
|
15883
17015
|
build_then_clear: {
|
|
15884
17016
|
artifactRequirements: {},
|
|
15885
17017
|
cleanupPolicy: "clear",
|
|
@@ -15913,6 +17045,27 @@ const LOOP_TEMPLATES = {
|
|
|
15913
17045
|
stopConditions: ["max_iterations", "required_evidence"],
|
|
15914
17046
|
tags: ["repo", "ci"],
|
|
15915
17047
|
},
|
|
17048
|
+
ship_it_loop: {
|
|
17049
|
+
artifactRequirements: SHIP_IT_ARTIFACT_REQUIREMENTS,
|
|
17050
|
+
cleanupPolicy: "clear",
|
|
17051
|
+
description: "Require branch, push, PR, CI, mergeability, manager merge decision, merge, post-merge, and adversarial evidence before ship-it continuation.",
|
|
17052
|
+
maxIterations: 2,
|
|
17053
|
+
name: "ship_it_loop",
|
|
17054
|
+
recommendedTools: ["gh", "verification.run_tests", "git"],
|
|
17055
|
+
requiredBeforeContinue: [
|
|
17056
|
+
"branch_ready",
|
|
17057
|
+
"branch_pushed",
|
|
17058
|
+
"pr_url",
|
|
17059
|
+
"ci_green",
|
|
17060
|
+
"mergeability_clean",
|
|
17061
|
+
"manager_merge_decision",
|
|
17062
|
+
"merge",
|
|
17063
|
+
"post_merge_verification",
|
|
17064
|
+
"adversarial_check",
|
|
17065
|
+
],
|
|
17066
|
+
stopConditions: ["max_iterations", "required_evidence", "manager_accepts"],
|
|
17067
|
+
tags: ["repo", "ci", "merge", "ship_it"],
|
|
17068
|
+
},
|
|
15916
17069
|
test_coverage_loop: {
|
|
15917
17070
|
artifactRequirements: { adversarial_check: ADVERSARIAL_CHECK_REQUIREMENT },
|
|
15918
17071
|
cleanupPolicy: "clear",
|
|
@@ -16035,6 +17188,9 @@ const MANAGER_RECIPES = {
|
|
|
16035
17188
|
"PR/CI/merge or satisfied_on_main proof",
|
|
16036
17189
|
"parent receipt update before the next child",
|
|
16037
17190
|
],
|
|
17191
|
+
finalReportRequirements: [
|
|
17192
|
+
"Record manager closeout proof, including final task state and any finish-task/heartbeat teardown receipt, in the final report instead of accepted worker criteria.",
|
|
17193
|
+
],
|
|
16038
17194
|
guidelines: [
|
|
16039
17195
|
"Keep exactly one child board active at a time.",
|
|
16040
17196
|
"Before activating the next child, update the parent receipt.",
|
|
@@ -16058,6 +17214,9 @@ const MANAGER_RECIPES = {
|
|
|
16058
17214
|
displayName: "Nudge / What's Next Manager",
|
|
16059
17215
|
epilogues: [],
|
|
16060
17216
|
evidenceGates: ["manager decision", "worker receipt", "accepted criteria closure"],
|
|
17217
|
+
finalReportRequirements: [
|
|
17218
|
+
"Record status, residual risk, and any finish-task or terminal closeout proof in the final report, not as worker acceptance criteria.",
|
|
17219
|
+
],
|
|
16061
17220
|
guidelines: [
|
|
16062
17221
|
"Prefer wait over nudge while the worker is active.",
|
|
16063
17222
|
"Ask for must-have current-task criteria versus follow-ups when scope changes.",
|
|
@@ -16085,6 +17244,9 @@ const MANAGER_RECIPES = {
|
|
|
16085
17244
|
displayName: "PR/CI/Merge Ralph Loop",
|
|
16086
17245
|
epilogues: ["draft-pr", "record-handoff"],
|
|
16087
17246
|
evidenceGates: ["pr_url", "ci_green", "merge", "adversarial_check"],
|
|
17247
|
+
finalReportRequirements: [
|
|
17248
|
+
"Record PR URL, CI, merge, handoff, finish-task, and cleanup receipts in the final report; keep accepted criteria focused on deliverable proof.",
|
|
17249
|
+
],
|
|
16088
17250
|
guidelines: ["Merge only after green CI and recorded manager decision evidence."],
|
|
16089
17251
|
loopTemplate: "pr_ci_merge_loop",
|
|
16090
17252
|
mode: "strict",
|
|
@@ -16094,6 +17256,57 @@ const MANAGER_RECIPES = {
|
|
|
16094
17256
|
supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
|
|
16095
17257
|
tools: ["verification.run_tests", "context.fetch_prs"],
|
|
16096
17258
|
},
|
|
17259
|
+
"ship-it-loop": {
|
|
17260
|
+
acceptance: [
|
|
17261
|
+
"Branch, push, PR URL, CI-green, mergeability, manager merge decision, merge, post-merge verification, and adversarial proof are recorded.",
|
|
17262
|
+
"Push, PR creation, conflict resolution, and merge actions are each gated by explicit manager permissions.",
|
|
17263
|
+
"Merge readiness is a manager decision after independent verification, not a worker claim or CI-green shortcut.",
|
|
17264
|
+
],
|
|
17265
|
+
cleanup: "clear after saved handoff",
|
|
17266
|
+
description: "Drive a visible manager-worker ship-it loop through branch push, PR, CI, conflict handling, manager merge decision, merge, and post-merge receipts.",
|
|
17267
|
+
disallowedActions: [
|
|
17268
|
+
"Do not push branches before repo.push_branch is permitted.",
|
|
17269
|
+
"Do not open or update PRs before repo.open_pr is permitted.",
|
|
17270
|
+
"Do not resolve conflicts before repo.resolve_conflicts is permitted and retry bounds are recorded.",
|
|
17271
|
+
"Do not merge before repo.merge_green_pr is permitted, CI is green, mergeability is clean, and the manager records merge_ready.",
|
|
17272
|
+
],
|
|
17273
|
+
displayName: "Autonomous Ship-It Loop",
|
|
17274
|
+
epilogues: ["draft-pr", "record-handoff"],
|
|
17275
|
+
evidenceGates: [
|
|
17276
|
+
"branch_ready",
|
|
17277
|
+
"branch_pushed",
|
|
17278
|
+
"pr_url",
|
|
17279
|
+
"ci_green",
|
|
17280
|
+
"mergeability_clean",
|
|
17281
|
+
"manager_merge_decision",
|
|
17282
|
+
"merge",
|
|
17283
|
+
"post_merge_verification",
|
|
17284
|
+
"adversarial_check",
|
|
17285
|
+
],
|
|
17286
|
+
finalReportRequirements: [
|
|
17287
|
+
"Record branch, PR URL, CI/check output, mergeability/conflict status, manager merge decision, merge SHA, post-merge verification, finish-task, and heartbeat teardown proof in the final report.",
|
|
17288
|
+
],
|
|
17289
|
+
guidelines: [
|
|
17290
|
+
"Keep all PR lifecycle phases visible in the manager and worker sessions.",
|
|
17291
|
+
"Treat CI-green, mergeability, and worker receipts as claims until the manager verifies them.",
|
|
17292
|
+
"Use a bounded conflict retry and block with evidence when conflicts remain unresolved.",
|
|
17293
|
+
],
|
|
17294
|
+
loopTemplate: "ship_it_loop",
|
|
17295
|
+
mode: "strict",
|
|
17296
|
+
name: "ship-it-loop",
|
|
17297
|
+
objective: "Supervise a worker from implementation through explicit branch, PR, CI, conflict, merge, and post-merge evidence gates.",
|
|
17298
|
+
permissions: [
|
|
17299
|
+
"repo.push_branch",
|
|
17300
|
+
"repo.open_pr",
|
|
17301
|
+
"repo.monitor_ci",
|
|
17302
|
+
"repo.resolve_conflicts",
|
|
17303
|
+
"repo.merge_green_pr",
|
|
17304
|
+
"worker_session.compact",
|
|
17305
|
+
"worker_session.clear",
|
|
17306
|
+
],
|
|
17307
|
+
supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
|
|
17308
|
+
tools: ["gh", "git", "verification.run_tests", "context.fetch_prs"],
|
|
17309
|
+
},
|
|
16097
17310
|
"test-coverage-loop": {
|
|
16098
17311
|
acceptance: [
|
|
16099
17312
|
"Coverage or targeted test evidence is recorded before another worker pass.",
|
|
@@ -16105,6 +17318,9 @@ const MANAGER_RECIPES = {
|
|
|
16105
17318
|
displayName: "Test Coverage Loop",
|
|
16106
17319
|
epilogues: [],
|
|
16107
17320
|
evidenceGates: ["test_coverage", "adversarial_check"],
|
|
17321
|
+
finalReportRequirements: [
|
|
17322
|
+
"Record final closeout and finish-task proof in the manager final report; do not make closeout mechanics a test-coverage criterion.",
|
|
17323
|
+
],
|
|
16108
17324
|
guidelines: ["Record coverage evidence before asking for another worker pass."],
|
|
16109
17325
|
loopTemplate: "test_coverage_loop",
|
|
16110
17326
|
mode: "strict",
|
|
@@ -16131,6 +17347,9 @@ const MANAGER_RECIPES = {
|
|
|
16131
17347
|
"diff_below_threshold",
|
|
16132
17348
|
"adversarial_check",
|
|
16133
17349
|
],
|
|
17350
|
+
finalReportRequirements: [
|
|
17351
|
+
"Record final visual decision, closeout, and cleanup proof in the manager final report; keep accepted criteria focused on visible-output evidence.",
|
|
17352
|
+
],
|
|
16134
17353
|
guidelines: ["Compare visible output against references before requesting another pass."],
|
|
16135
17354
|
loopTemplate: "visual_diff_loop",
|
|
16136
17355
|
mode: "guided",
|
|
@@ -16149,6 +17368,9 @@ const MANAGER_RECIPE_ALIASES = {
|
|
|
16149
17368
|
"pr ci merge ralph loop": "pr-ci-merge-ralph-loop",
|
|
16150
17369
|
"pr/ci/merge ralph loop": "pr-ci-merge-ralph-loop",
|
|
16151
17370
|
"ralph loop": "pr-ci-merge-ralph-loop",
|
|
17371
|
+
"ship it": "ship-it-loop",
|
|
17372
|
+
"ship it loop": "ship-it-loop",
|
|
17373
|
+
"ship-it": "ship-it-loop",
|
|
16152
17374
|
"test coverage": "test-coverage-loop",
|
|
16153
17375
|
"test coverage loop": "test-coverage-loop",
|
|
16154
17376
|
"ux polish": "ux-polish-loop",
|
|
@@ -16182,6 +17404,7 @@ function managerRecipeSummary(name) {
|
|
|
16182
17404
|
display_name: recipe.displayName,
|
|
16183
17405
|
epilogues: [...recipe.epilogues],
|
|
16184
17406
|
evidence_gates: [...recipe.evidenceGates],
|
|
17407
|
+
final_report_requirements: [...recipe.finalReportRequirements],
|
|
16185
17408
|
guidelines: [...recipe.guidelines],
|
|
16186
17409
|
locked_summary_template: lockedManagerRecipeSummary(recipe),
|
|
16187
17410
|
loop_template: recipe.loopTemplate,
|
|
@@ -16236,6 +17459,7 @@ function lockedManagerRecipeSummary(recipe) {
|
|
|
16236
17459
|
`Epilogues: ${recipe.epilogues.length > 0 ? recipe.epilogues.join(", ") : "none"}`,
|
|
16237
17460
|
`Cleanup: ${recipe.cleanup}`,
|
|
16238
17461
|
`Evidence gates: ${recipe.evidenceGates.length > 0 ? recipe.evidenceGates.join(", ") : "manager-reviewed evidence"}`,
|
|
17462
|
+
`Final report: ${recipe.finalReportRequirements.join("; ")}`,
|
|
16239
17463
|
`Not allowed: ${recipe.disallowedActions.length > 0 ? recipe.disallowedActions.join("; ") : "unconfirmed custom actions"}`,
|
|
16240
17464
|
"User confirmed: <yes|no>",
|
|
16241
17465
|
].join("\n");
|
|
@@ -16542,7 +17766,7 @@ function loopStatusSummarySync(database, options) {
|
|
|
16542
17766
|
const matchingCommands = commandRows.filter((row) => commandRowMatchesRun(row, options.run.id));
|
|
16543
17767
|
const commandStates = countBy(matchingCommands.map((row) => row.state));
|
|
16544
17768
|
const notificationRows = database.prepare(`
|
|
16545
|
-
select state, payload_json
|
|
17769
|
+
select consumed_at, state, payload_json
|
|
16546
17770
|
from routed_notifications
|
|
16547
17771
|
where task_id = ?
|
|
16548
17772
|
order by created_at, id
|
|
@@ -16575,6 +17799,12 @@ function loopStatusSummarySync(database, options) {
|
|
|
16575
17799
|
.filter((value) => typeof value === "string" && value.length > 0))].sort();
|
|
16576
17800
|
const telemetryEvents = telemetryEventsForRunSync(database, { runId: options.run.id, taskId: options.task.id });
|
|
16577
17801
|
const telemetryByType = countBy(telemetryEvents.map((event) => event.event_type));
|
|
17802
|
+
const appTaskDispatch = appTaskDispatchSummarySync(database, {
|
|
17803
|
+
commandRows,
|
|
17804
|
+
notificationRows,
|
|
17805
|
+
runScopedActivityTotal: matchingCommands.length + matchingNotifications.length + telemetryEvents.length,
|
|
17806
|
+
taskId: options.task.id,
|
|
17807
|
+
});
|
|
16578
17808
|
const failedCommandCount = commandStates.failed ?? 0;
|
|
16579
17809
|
const failureCounts = loopFailureCountsSync(database, {
|
|
16580
17810
|
failedCommandCount,
|
|
@@ -16595,6 +17825,7 @@ function loopStatusSummarySync(database, options) {
|
|
|
16595
17825
|
total: evidenceItems.length,
|
|
16596
17826
|
types: evidenceTypes,
|
|
16597
17827
|
},
|
|
17828
|
+
app_task_dispatch: appTaskDispatch,
|
|
16598
17829
|
failures: failureCounts,
|
|
16599
17830
|
inbox: {
|
|
16600
17831
|
worker_unconsumed: workerInbox,
|
|
@@ -16638,6 +17869,52 @@ function telemetryEventsForRunSync(database, options) {
|
|
|
16638
17869
|
limit 1000
|
|
16639
17870
|
`).all(options.taskId, options.runId);
|
|
16640
17871
|
}
|
|
17872
|
+
function appTaskDispatchSummarySync(database, options) {
|
|
17873
|
+
const taskDispatchEventTypes = [
|
|
17874
|
+
"app_autopilot_started",
|
|
17875
|
+
"app_autopilot_stopped",
|
|
17876
|
+
"app_heartbeat",
|
|
17877
|
+
"app_wakeup_delivery_recorded",
|
|
17878
|
+
"app_wakeup_dispatch_planned",
|
|
17879
|
+
"command_created",
|
|
17880
|
+
"dispatch_inbox_consumed",
|
|
17881
|
+
];
|
|
17882
|
+
const telemetryRows = database.prepare(`
|
|
17883
|
+
select event_type, timestamp
|
|
17884
|
+
from telemetry_events
|
|
17885
|
+
where task_id = ?
|
|
17886
|
+
and event_type in (${taskDispatchEventTypes.map(() => "?").join(", ")})
|
|
17887
|
+
order by timestamp, id
|
|
17888
|
+
`).all(options.taskId, ...taskDispatchEventTypes);
|
|
17889
|
+
const telemetryByType = countBy(telemetryRows.map((row) => row.event_type));
|
|
17890
|
+
const commandStates = countBy(options.commandRows.map((row) => row.state));
|
|
17891
|
+
const notificationStates = countBy(options.notificationRows.map((row) => row.state));
|
|
17892
|
+
const recordsTotal = options.commandRows.length + options.notificationRows.length + telemetryRows.length;
|
|
17893
|
+
const blindToRun = options.runScopedActivityTotal === 0 && recordsTotal > 0;
|
|
17894
|
+
return {
|
|
17895
|
+
commands: {
|
|
17896
|
+
states: sortJson(commandStates),
|
|
17897
|
+
total: options.commandRows.length,
|
|
17898
|
+
},
|
|
17899
|
+
latest_event_at: telemetryRows.at(-1)?.timestamp ?? null,
|
|
17900
|
+
note: blindToRun
|
|
17901
|
+
? "Requested run has no run-scoped activity, but task-level app Dispatch records exist."
|
|
17902
|
+
: null,
|
|
17903
|
+
notifications: {
|
|
17904
|
+
delivered_unconsumed: options.notificationRows
|
|
17905
|
+
.filter((row) => row.state === "delivered" && row.consumed_at === null).length,
|
|
17906
|
+
states: sortJson(notificationStates),
|
|
17907
|
+
total: options.notificationRows.length,
|
|
17908
|
+
},
|
|
17909
|
+
records_total: recordsTotal,
|
|
17910
|
+
telemetry: {
|
|
17911
|
+
by_event_type: sortJson(telemetryByType),
|
|
17912
|
+
command_created: telemetryByType.command_created ?? 0,
|
|
17913
|
+
dispatch_inbox_consumed: telemetryByType.dispatch_inbox_consumed ?? 0,
|
|
17914
|
+
total: telemetryRows.length,
|
|
17915
|
+
},
|
|
17916
|
+
};
|
|
17917
|
+
}
|
|
16641
17918
|
function loopFailureCountsSync(database, options) {
|
|
16642
17919
|
const failedCycles = database.prepare(`
|
|
16643
17920
|
select count(distinct mc.id) as count
|
|
@@ -16727,6 +18004,8 @@ function renderLoopStatusText(result) {
|
|
|
16727
18004
|
const notifications = result.notifications;
|
|
16728
18005
|
const inbox = result.inbox;
|
|
16729
18006
|
const telemetry = result.telemetry;
|
|
18007
|
+
const appTaskDispatch = result.app_task_dispatch;
|
|
18008
|
+
const appTaskDispatchTelemetry = appTaskDispatch?.telemetry;
|
|
16730
18009
|
return [
|
|
16731
18010
|
`task: ${task.name} (${task.state})`,
|
|
16732
18011
|
`run: ${run.name || run.id} (${run.status})`,
|
|
@@ -16735,6 +18014,7 @@ function renderLoopStatusText(result) {
|
|
|
16735
18014
|
`notifications: ${notifications.delivered}/${notifications.total} delivered`,
|
|
16736
18015
|
`worker_unconsumed: ${inbox.worker_unconsumed}`,
|
|
16737
18016
|
`dispatch_inbox_consumed: ${telemetry.dispatch_inbox_consumed}`,
|
|
18017
|
+
`app_task_dispatch: ${appTaskDispatch?.records_total ?? 0} records ${JSON.stringify(appTaskDispatchTelemetry?.by_event_type ?? {})}${appTaskDispatch?.note ? ` (${appTaskDispatch.note})` : ""}`,
|
|
16738
18018
|
`failures: ${JSON.stringify(result.failures ?? {})}`,
|
|
16739
18019
|
`recommendation: ${result.recommendation}`,
|
|
16740
18020
|
].join("\n") + "\n";
|
|
@@ -17250,6 +18530,7 @@ function disposableWorkerHandoff(taskName, runName, dbPath) {
|
|
|
17250
18530
|
"",
|
|
17251
18531
|
`You are the worker for task ${taskName}${loopClause}.`,
|
|
17252
18532
|
"Keep polling your Conveyor worker inbox until there are no items left or the loop reaches max_iterations. Consume the next item now, treat each consumed item as the manager's next instruction, complete the requested work, and report changed files, exact commands run, evidence, and any residual risk.",
|
|
18533
|
+
...visibleSessionProtocolLines("worker"),
|
|
17253
18534
|
"After completing or blocking on a consumed item, send the manager a durable Conveyor notification before your final answer. A direct app-thread final answer is not a manager receipt and is not task completion.",
|
|
17254
18535
|
`Run: ${notifyCommand}`,
|
|
17255
18536
|
`Then run: ${dispatchCommand}`,
|
|
@@ -17332,9 +18613,11 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
17332
18613
|
`After a successful app-thread send, record it with: ${deliveryReceiptCommands.sent}`,
|
|
17333
18614
|
`For healthy skipped actions, record: ${deliveryReceiptCommands.skipped}`,
|
|
17334
18615
|
`For missing-thread blocked actions, record: ${deliveryReceiptCommands.blocked}`,
|
|
18616
|
+
...visibleSessionProtocolLines("manager"),
|
|
17335
18617
|
"If an item is consumed, execute only that manager instruction, verify worker claims before recording conclusions, update Conveyor state as appropriate, and produce exactly one next worker task.",
|
|
17336
18618
|
"If no item is consumed, stop after a one-line idle receipt.",
|
|
17337
18619
|
"Do not delete, pause, or disable manager or worker heartbeat automation after an idle poll; an idle poll is only a quiet interval.",
|
|
18620
|
+
"Keep manager closeout/control-plane proof out of accepted worker criteria; record finish-task, final task state, and heartbeat teardown proof in the manager final report or audit receipts.",
|
|
17338
18621
|
`If all accepted criteria are satisfied, deferred, or rejected and there is no next worker task, record the terminal manager decision, run or report the result of: ${terminalCloseoutCommand}`,
|
|
17339
18622
|
"After verified task closeout, explicitly report heartbeat teardown status; if the task remains managed/active, report that as a control-plane blocker instead of calling the loop complete.",
|
|
17340
18623
|
].join("\n"),
|
|
@@ -17348,6 +18631,7 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
17348
18631
|
`Run the worker app heartbeat for task ${taskName}.`,
|
|
17349
18632
|
`Run: ${workerHeartbeatCommand}`,
|
|
17350
18633
|
`If the heartbeat output asks for direct inbox polling, run: ${workerInboxCommand}`,
|
|
18634
|
+
...visibleSessionProtocolLines("worker"),
|
|
17351
18635
|
"If an item is consumed, execute only that single worker instruction and return exact commands, compact evidence for any completion claim, blockers/residual risk, and exactly one next recommended worker task.",
|
|
17352
18636
|
"Before your final answer after any consumed item, notify the manager durably; a direct app-thread final answer is not a manager receipt and is not task completion.",
|
|
17353
18637
|
`Run: ${workerNotifyCommand}`,
|
|
@@ -17583,6 +18867,7 @@ function startManagerBootstrapPrompt(database, options) {
|
|
|
17583
18867
|
"- Treat acceptance criteria as living supervision state.",
|
|
17584
18868
|
"- Inspect `manager_context.acceptance_criteria` each cycle.",
|
|
17585
18869
|
"- If worker progress reveals new edge cases, tests, polish, or scope boundaries, ask the worker to propose must-have vs follow-up criteria.",
|
|
18870
|
+
"- Keep manager closeout/control-plane proof out of accepted worker criteria; record finish-task, final task state, teardown, and final-report proof in manager closeout evidence instead.",
|
|
17586
18871
|
"- Before finishing, compare worker receipts/verification against accepted open criteria.",
|
|
17587
18872
|
`- For each accepted criterion that is proven, record evidence with \`${satisfyCriterionCommand}\`.`,
|
|
17588
18873
|
`- When all accepted criteria are satisfied, deferred, or rejected, finish the task with \`${workerctl} finish-task ${taskLine} --reason "Accepted criteria satisfied" --require-criteria-audit${pathSuffix}\`.`,
|