agent-conveyor 0.1.12 → 0.1.14
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 +74 -15
- package/dist/cli/typescript-runtime.js +598 -13
- package/dist/cli/typescript-runtime.js.map +1 -1
- package/dist/runtime/app-autonomy.d.ts +12 -0
- package/dist/runtime/app-autonomy.js +119 -0
- package/dist/runtime/app-autonomy.js.map +1 -1
- package/dist/runtime/manager-permissions.js +1 -1
- package/dist/runtime/manager-permissions.js.map +1 -1
- package/docs/manager-recipes.md +90 -0
- package/package.json +1 -1
- package/skills/manage-codex-workers/SKILL.md +93 -10
|
@@ -5,7 +5,7 @@ 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
10
|
import { exportTaskSync } from "../runtime/export.js";
|
|
11
11
|
import { ingestSessionSync } from "../runtime/ingest.js";
|
|
@@ -368,10 +368,10 @@ function commandHelpText(program, command) {
|
|
|
368
368
|
` ${program} criteria my-task --satisfy 1 --proof "File exists" --evidence-json '{"artifact":{"path":"docs/note.md"}}' --path /tmp/work/workerctl.db`,
|
|
369
369
|
],
|
|
370
370
|
"finish-task": [
|
|
371
|
-
`usage: ${program} finish-task <task> --reason <reason> [--require-criteria-audit] ${path}`,
|
|
371
|
+
`usage: ${program} finish-task <task> --reason <reason> [--require-criteria-audit] ${path} [--json]`,
|
|
372
372
|
"",
|
|
373
373
|
"Examples:",
|
|
374
|
-
` ${program} finish-task my-task --reason "Accepted criteria satisfied" --require-criteria-audit --path /tmp/work/workerctl.db`,
|
|
374
|
+
` ${program} finish-task my-task --reason "Accepted criteria satisfied" --require-criteria-audit --path /tmp/work/workerctl.db --json`,
|
|
375
375
|
],
|
|
376
376
|
"manager-ack": [
|
|
377
377
|
`usage: ${program} manager-ack <task> --from-stdin ${path}`,
|
|
@@ -389,7 +389,7 @@ function commandHelpText(program, command) {
|
|
|
389
389
|
` ${program} dispatch --once --type nudge_worker --path /tmp/work/workerctl.db`,
|
|
390
390
|
],
|
|
391
391
|
"app-autopilot": [
|
|
392
|
-
`usage: ${program} app-autopilot start|stop|status <task> [--dispatcher-id ID] [--interval SECONDS] [--watch-iterations N] [--stale-after N] ${path} [--json]`,
|
|
392
|
+
`usage: ${program} app-autopilot start|stop|status <task> [--dispatcher-id ID] [--interval SECONDS] [--watch-iterations N] [--stale-after N] [--quiet-after N] ${path} [--json]`,
|
|
393
393
|
"",
|
|
394
394
|
"Manage the app-native heartbeat policy for a bound manager/worker pair.",
|
|
395
395
|
"The CLI records policy receipts and emits Codex app heartbeat automation specs; app-thread automation creation still happens through Codex app tools.",
|
|
@@ -528,6 +528,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
528
528
|
source: null,
|
|
529
529
|
proof: null,
|
|
530
530
|
purpose: null,
|
|
531
|
+
quietAfterCycles: 3,
|
|
531
532
|
questions: false,
|
|
532
533
|
rationale: null,
|
|
533
534
|
receiptOutput: null,
|
|
@@ -2496,6 +2497,21 @@ function parseRuntimeArgs(args, env) {
|
|
|
2496
2497
|
flags.appStaleAfterSeconds = value;
|
|
2497
2498
|
index += 1;
|
|
2498
2499
|
}
|
|
2500
|
+
else if (arg === "--quiet-after") {
|
|
2501
|
+
if (command !== "app-autopilot") {
|
|
2502
|
+
return { command, enabled, error: "Unsupported TypeScript runtime option: --quiet-after", explicit, flags, task };
|
|
2503
|
+
}
|
|
2504
|
+
const parsedValue = valueAfter(queue, index, arg);
|
|
2505
|
+
if (parsedValue.error) {
|
|
2506
|
+
return { command, enabled, error: parsedValue.error, explicit, flags, task };
|
|
2507
|
+
}
|
|
2508
|
+
const value = Number(parsedValue.value);
|
|
2509
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2510
|
+
return { command, enabled, error: "--quiet-after must be a non-negative integer.", explicit, flags, task };
|
|
2511
|
+
}
|
|
2512
|
+
flags.quietAfterCycles = value;
|
|
2513
|
+
index += 1;
|
|
2514
|
+
}
|
|
2499
2515
|
else if (arg === "--terminal-stale-seconds") {
|
|
2500
2516
|
if (command !== "idle-check") {
|
|
2501
2517
|
return { command, enabled, error: "Unsupported TypeScript runtime option: --terminal-stale-seconds", explicit, flags, task };
|
|
@@ -2838,7 +2854,7 @@ function parseRuntimeArgs(args, env) {
|
|
|
2838
2854
|
}
|
|
2839
2855
|
}
|
|
2840
2856
|
else if (command === "loop-evidence" && flags.subtype === null) {
|
|
2841
|
-
if (!["add", "visual-diff", "visual_diff", "adversarial-check", "adversarial_check"].includes(arg)) {
|
|
2857
|
+
if (!["add", "visual-diff", "visual_diff", "build-passed", "build_passed", "adversarial-check", "adversarial_check"].includes(arg)) {
|
|
2842
2858
|
return { command, enabled, error: `Unsupported loop-evidence action: ${arg}`, explicit, flags, task };
|
|
2843
2859
|
}
|
|
2844
2860
|
flags.subtype = arg;
|
|
@@ -3148,7 +3164,7 @@ function runLoopEvidenceCommand(parsed, options) {
|
|
|
3148
3164
|
}
|
|
3149
3165
|
const action = parsed.flags.subtype;
|
|
3150
3166
|
if (!action) {
|
|
3151
|
-
return unsupportedRuntimeResult(parsed, "loop-evidence requires an action: add, visual-diff, or adversarial-check.");
|
|
3167
|
+
return unsupportedRuntimeResult(parsed, "loop-evidence requires an action: add, visual-diff, build-passed, or adversarial-check.");
|
|
3152
3168
|
}
|
|
3153
3169
|
const task = requireTask(parsed);
|
|
3154
3170
|
if (!parsed.flags.loopRun) {
|
|
@@ -3175,6 +3191,24 @@ function runLoopEvidenceCommand(parsed, options) {
|
|
|
3175
3191
|
});
|
|
3176
3192
|
return jsonResult(result);
|
|
3177
3193
|
}
|
|
3194
|
+
if (action === "build-passed" || action === "build_passed") {
|
|
3195
|
+
if (parsed.flags.evidenceType && parsed.flags.evidenceType !== "build_passed") {
|
|
3196
|
+
return errorResult("loop-evidence build-passed records evidence_type=build_passed; omit --evidence-type or use build_passed.");
|
|
3197
|
+
}
|
|
3198
|
+
const result = recordLoopEvidenceSync(database, {
|
|
3199
|
+
artifactPath: parsed.flags.output,
|
|
3200
|
+
correlationId: parsed.flags.correlationId,
|
|
3201
|
+
evidenceType: "build_passed",
|
|
3202
|
+
iteration: parsed.flags.currentIteration,
|
|
3203
|
+
loopRunId: parsed.flags.loopRun,
|
|
3204
|
+
metadata: jsonObjectArg(parsed.flags.metadataJson, "--metadata-json"),
|
|
3205
|
+
proof: parsed.flags.proof,
|
|
3206
|
+
source,
|
|
3207
|
+
status: parsed.flags.statusState ?? "pass",
|
|
3208
|
+
task,
|
|
3209
|
+
});
|
|
3210
|
+
return jsonResult(result);
|
|
3211
|
+
}
|
|
3178
3212
|
if (action === "adversarial-check" || action === "adversarial_check") {
|
|
3179
3213
|
const result = recordAdversarialLoopEvidenceSync(database, {
|
|
3180
3214
|
artifactPath: parsed.flags.output,
|
|
@@ -3738,6 +3772,7 @@ function runAppAutopilotCommand(parsed, options) {
|
|
|
3738
3772
|
heartbeatIntervalMinutes: 2,
|
|
3739
3773
|
heartbeatStaleSeconds: parsed.flags.appStaleAfterSeconds,
|
|
3740
3774
|
now: timestamp,
|
|
3775
|
+
quietAfterCycles: parsed.flags.quietAfterCycles,
|
|
3741
3776
|
taskName,
|
|
3742
3777
|
watchIterations: parsed.flags.watchIterations ?? 1000000,
|
|
3743
3778
|
});
|
|
@@ -3759,6 +3794,7 @@ function runAppAutopilotCommand(parsed, options) {
|
|
|
3759
3794
|
dispatcher_command: plan.control.dispatcher_command,
|
|
3760
3795
|
dispatcher_id: dispatcherId,
|
|
3761
3796
|
interval_minutes: plan.interval_minutes,
|
|
3797
|
+
quiescence: plan.quiescence,
|
|
3762
3798
|
summary: plan.summary,
|
|
3763
3799
|
},
|
|
3764
3800
|
correlation: {
|
|
@@ -3918,6 +3954,13 @@ function renderAppAutopilotText(result) {
|
|
|
3918
3954
|
`Dispatch command: ${result.plan.control.dispatcher_command}`,
|
|
3919
3955
|
`Wake dispatch: ${result.plan.control.wakeup_dispatch_command}`,
|
|
3920
3956
|
];
|
|
3957
|
+
if (result.plan.quiescence.recommended_action === "stop_autopilot") {
|
|
3958
|
+
lines.push(`Quiescence: stop recommended - ${result.plan.quiescence.reason}`);
|
|
3959
|
+
lines.push(`Stop command: ${result.plan.control.stop_command}`);
|
|
3960
|
+
}
|
|
3961
|
+
else {
|
|
3962
|
+
lines.push(`Quiescence: ${result.plan.quiescence.state} (${result.plan.quiescence.quiet_cycles}/${result.plan.quiescence.threshold_cycles} quiet cycles)`);
|
|
3963
|
+
}
|
|
3921
3964
|
if (result.receipt) {
|
|
3922
3965
|
lines.push(`Receipt: ${result.receipt.event_type} ${result.receipt.event_id}`);
|
|
3923
3966
|
}
|
|
@@ -4024,6 +4067,7 @@ const QA_PLAN_SCENARIOS = new Set([
|
|
|
4024
4067
|
"tmux-errors",
|
|
4025
4068
|
"dispatch-completion",
|
|
4026
4069
|
"ralph-loop",
|
|
4070
|
+
"ship-it-loop",
|
|
4027
4071
|
"adversarial-triggers",
|
|
4028
4072
|
"goalbuddy-conveyor",
|
|
4029
4073
|
]);
|
|
@@ -4157,6 +4201,52 @@ function qaPlan(scenario) {
|
|
|
4157
4201
|
],
|
|
4158
4202
|
};
|
|
4159
4203
|
}
|
|
4204
|
+
if (scenario === "ship-it-loop") {
|
|
4205
|
+
return {
|
|
4206
|
+
authority_boundaries: [
|
|
4207
|
+
"Do not push a branch before repo.push_branch is permitted.",
|
|
4208
|
+
"Do not open or update a PR before repo.open_pr is permitted.",
|
|
4209
|
+
"Do not treat CI monitoring as CI truth; record explicit ci_green evidence.",
|
|
4210
|
+
"Do not resolve conflicts without a bounded manager instruction and retry limit.",
|
|
4211
|
+
"Do not merge before repo.merge_green_pr, ci_green, mergeability, manager_merge_decision, merge, post_merge_verification, and adversarial_check evidence exist.",
|
|
4212
|
+
],
|
|
4213
|
+
correlation_markers: [
|
|
4214
|
+
{ correlation_id: "ship-it-push-permission", purpose: "push branch permission gate" },
|
|
4215
|
+
{ correlation_id: "ship-it-open-pr-permission", purpose: "open PR permission gate" },
|
|
4216
|
+
{ correlation_id: "ship-it-merge-permission", purpose: "merge permission gate" },
|
|
4217
|
+
{ correlation_id: "ship-it-missing-evidence", purpose: "missing lifecycle evidence block" },
|
|
4218
|
+
{ correlation_id: "ship-it-conflict-block", purpose: "conflict retry limit proof" },
|
|
4219
|
+
{ correlation_id: "ship-it-allowed-closeout", purpose: "allowed closeout after all lifecycle evidence" },
|
|
4220
|
+
],
|
|
4221
|
+
evidence_template: {
|
|
4222
|
+
branch_ready: { branch: "<branch>", commit_sha: "<sha>" },
|
|
4223
|
+
branch_pushed: { remote: "origin", branch: "<branch>" },
|
|
4224
|
+
pr_url: { url: "<pull request URL>" },
|
|
4225
|
+
ci_green: { command: "gh pr checks --required", status: "green" },
|
|
4226
|
+
mergeability_clean: { conflicts: false, mergeable_state: "clean" },
|
|
4227
|
+
manager_merge_decision: { decision: "merge_ready", manager_verified: true },
|
|
4228
|
+
merge: { merge_sha: "<sha>" },
|
|
4229
|
+
post_merge_verification: { command: "<post-merge check>", status: "pass" },
|
|
4230
|
+
adversarial_check: { failure_mode: "<risk>", check: "<proof>", result: "<outcome>" },
|
|
4231
|
+
},
|
|
4232
|
+
expected_observations: [
|
|
4233
|
+
"push, PR creation, and merge commands fail closed until their manager permissions are granted",
|
|
4234
|
+
"missing lifecycle evidence blocks a continue_iteration before worker delivery",
|
|
4235
|
+
"unresolved conflicts are represented as bounded blockers, not hidden behind CI green",
|
|
4236
|
+
"a fresh retry delivers only after branch, PR, CI, mergeability, manager decision, merge, post-merge, and adversarial evidence exists",
|
|
4237
|
+
"the recipe and prompts keep merge readiness as a manager decision, not a worker claim",
|
|
4238
|
+
],
|
|
4239
|
+
scenario,
|
|
4240
|
+
steps: [
|
|
4241
|
+
"Create a disposable no-tmux task with the ship_it_loop template.",
|
|
4242
|
+
"Run the permission-gate checks for repo.push_branch, repo.open_pr, and repo.merge_green_pr.",
|
|
4243
|
+
"Attempt a lifecycle continuation before evidence and verify missing evidence blocks before worker delivery.",
|
|
4244
|
+
"Record partial PR/CI evidence and verify mergeability/manager-decision/merge/post-merge proof is still required.",
|
|
4245
|
+
"Record conflict retry-limit evidence as blocked when unresolved.",
|
|
4246
|
+
"Record all lifecycle receipts plus structured adversarial proof and verify a fresh retry reaches the worker inbox.",
|
|
4247
|
+
],
|
|
4248
|
+
};
|
|
4249
|
+
}
|
|
4160
4250
|
if (scenario === "adversarial-triggers") {
|
|
4161
4251
|
return {
|
|
4162
4252
|
correlation_markers: [
|
|
@@ -4257,6 +4347,7 @@ const SUPPORTED_QA_RUN_SCENARIOS = new Set([
|
|
|
4257
4347
|
"generic-loop-template",
|
|
4258
4348
|
"generic-loop-template-browser",
|
|
4259
4349
|
"ralph-loop-guardrails",
|
|
4350
|
+
"ship-it-loop",
|
|
4260
4351
|
"test-coverage-loop",
|
|
4261
4352
|
]);
|
|
4262
4353
|
function isSupportedQaRunScenario(scenario) {
|
|
@@ -4299,6 +4390,9 @@ function runQaScenario(scenario, context) {
|
|
|
4299
4390
|
if (scenario === "build-clear-loop") {
|
|
4300
4391
|
return qaRunBuildClearLoop(context);
|
|
4301
4392
|
}
|
|
4393
|
+
if (scenario === "ship-it-loop") {
|
|
4394
|
+
return qaRunShipItLoop(context);
|
|
4395
|
+
}
|
|
4302
4396
|
if (scenario === "adversarial-triggers") {
|
|
4303
4397
|
return qaRunAdversarialTriggers(context);
|
|
4304
4398
|
}
|
|
@@ -4644,6 +4738,166 @@ function qaRunBuildClearLoop(context) {
|
|
|
4644
4738
|
template_metadata: templateMetadata,
|
|
4645
4739
|
};
|
|
4646
4740
|
}
|
|
4741
|
+
function qaRunShipItLoop(context) {
|
|
4742
|
+
const slug = randomUUID().slice(0, 8);
|
|
4743
|
+
const checks = [];
|
|
4744
|
+
const generatedTasks = [];
|
|
4745
|
+
const pushTask = createQaBoundTask(context, slug, "ship-it-push-permission");
|
|
4746
|
+
generatedTasks.push(generatedTask(pushTask, "ship-it-push-permission"));
|
|
4747
|
+
checks.push(qaRunPermissionGate(context, pushTask, {
|
|
4748
|
+
checkName: "ship_it_push_branch_requires_repo_push_branch",
|
|
4749
|
+
correlationId: "ship-it-push-permission-denied",
|
|
4750
|
+
message: "Push branch origin/codex/ship-it-loop.",
|
|
4751
|
+
permission: "repo.push_branch",
|
|
4752
|
+
}));
|
|
4753
|
+
qaConfigureManagerPermissions(context, pushTask, ["repo.push_branch"]);
|
|
4754
|
+
checks.push(qaRunPermissionGate(context, pushTask, {
|
|
4755
|
+
checkName: "ship_it_push_branch_delivers_after_permission",
|
|
4756
|
+
correlationId: "ship-it-push-permission-allowed",
|
|
4757
|
+
expectAllowed: true,
|
|
4758
|
+
message: "Push branch origin/codex/ship-it-loop after manager permission.",
|
|
4759
|
+
permission: "repo.push_branch",
|
|
4760
|
+
}));
|
|
4761
|
+
const prTask = createQaBoundTask(context, slug, "ship-it-open-pr-permission");
|
|
4762
|
+
generatedTasks.push(generatedTask(prTask, "ship-it-open-pr-permission"));
|
|
4763
|
+
checks.push(qaRunPermissionGate(context, prTask, {
|
|
4764
|
+
checkName: "ship_it_open_pr_requires_repo_open_pr",
|
|
4765
|
+
correlationId: "ship-it-open-pr-permission-denied",
|
|
4766
|
+
message: "Open PR for ship-it loop.",
|
|
4767
|
+
permission: "repo.open_pr",
|
|
4768
|
+
}));
|
|
4769
|
+
qaConfigureManagerPermissions(context, prTask, ["repo.open_pr"]);
|
|
4770
|
+
checks.push(qaRunPermissionGate(context, prTask, {
|
|
4771
|
+
checkName: "ship_it_open_pr_delivers_after_permission",
|
|
4772
|
+
correlationId: "ship-it-open-pr-permission-allowed",
|
|
4773
|
+
expectAllowed: true,
|
|
4774
|
+
message: "Open PR for ship-it loop after manager permission.",
|
|
4775
|
+
permission: "repo.open_pr",
|
|
4776
|
+
}));
|
|
4777
|
+
const mergeTask = createQaBoundTask(context, slug, "ship-it-merge-permission");
|
|
4778
|
+
generatedTasks.push(generatedTask(mergeTask, "ship-it-merge-permission"));
|
|
4779
|
+
checks.push(qaRunPermissionGate(context, mergeTask, {
|
|
4780
|
+
checkName: "ship_it_merge_requires_repo_merge_green_pr",
|
|
4781
|
+
correlationId: "ship-it-merge-permission-denied",
|
|
4782
|
+
message: "Merge PR after verified closeout.",
|
|
4783
|
+
permission: "repo.merge_green_pr",
|
|
4784
|
+
}));
|
|
4785
|
+
qaConfigureManagerPermissions(context, mergeTask, ["repo.merge_green_pr"]);
|
|
4786
|
+
checks.push(qaRunPermissionGate(context, mergeTask, {
|
|
4787
|
+
checkName: "ship_it_merge_delivers_after_permission",
|
|
4788
|
+
correlationId: "ship-it-merge-permission-allowed",
|
|
4789
|
+
expectAllowed: true,
|
|
4790
|
+
message: "Merge PR after verified closeout and manager permission.",
|
|
4791
|
+
permission: "repo.merge_green_pr",
|
|
4792
|
+
}));
|
|
4793
|
+
const lifecycleTask = createQaBoundTask(context, slug, "ship-it-lifecycle");
|
|
4794
|
+
generatedTasks.push(generatedTask(lifecycleTask, "ship-it-lifecycle"));
|
|
4795
|
+
const templateMetadata = loopTemplateMetadata("ship_it_loop", {
|
|
4796
|
+
currentIteration: 1,
|
|
4797
|
+
maxIterations: 2,
|
|
4798
|
+
seedPromptSha256: "qa-run-ship-it-seed",
|
|
4799
|
+
});
|
|
4800
|
+
const run = createQaRalphLoopRun(context, lifecycleTask, {
|
|
4801
|
+
currentIteration: 1,
|
|
4802
|
+
maxIterations: 2,
|
|
4803
|
+
metadata: templateMetadata,
|
|
4804
|
+
preset: "ship_it_loop",
|
|
4805
|
+
requiredBeforeContinue: asStringArray(templateMetadata.required_before_continue),
|
|
4806
|
+
seedPromptSha256: "qa-run-ship-it-seed",
|
|
4807
|
+
stopConditions: asStringArray(templateMetadata.stop_conditions),
|
|
4808
|
+
});
|
|
4809
|
+
enqueueQaContinue(context, lifecycleTask, run.id, "ship-it-missing-evidence", "Run ship-it continuation before lifecycle evidence.");
|
|
4810
|
+
const missing = qaDispatchContinueOnce(context, "ship-it-missing-evidence");
|
|
4811
|
+
const missingCounts = qaDeliveryCounts(context, lifecycleTask);
|
|
4812
|
+
qaExpectBlocked(missing, missingCounts, {
|
|
4813
|
+
message: "ship_it_loop missing lifecycle evidence",
|
|
4814
|
+
missingEvidence: asStringArray(templateMetadata.required_before_continue),
|
|
4815
|
+
reason: "missing_required_evidence",
|
|
4816
|
+
});
|
|
4817
|
+
checks.push(qaCheck("ship_it_lifecycle_blocks_before_any_evidence", missing, missingCounts));
|
|
4818
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "branch_ready", "ship-it-branch-ready", {
|
|
4819
|
+
metadata: { branch: "codex/ship-it-loop", commit_sha: "1111111111111111111111111111111111111111" },
|
|
4820
|
+
});
|
|
4821
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "branch_pushed", "ship-it-branch-pushed", {
|
|
4822
|
+
metadata: { branch: "codex/ship-it-loop", remote: "origin" },
|
|
4823
|
+
});
|
|
4824
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "pr_url", "ship-it-pr-url", {
|
|
4825
|
+
metadata: { url: "https://github.example.test/acme/repo/pull/42" },
|
|
4826
|
+
});
|
|
4827
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "ci_green", "ship-it-ci-green", {
|
|
4828
|
+
metadata: { command: "gh pr checks 42 --required", status: "green" },
|
|
4829
|
+
status: "green",
|
|
4830
|
+
});
|
|
4831
|
+
enqueueQaContinue(context, lifecycleTask, run.id, "ship-it-partial-evidence", "Run ship-it continuation after PR and CI but before merge readiness.");
|
|
4832
|
+
const partial = qaDispatchContinueOnce(context, "ship-it-partial-evidence");
|
|
4833
|
+
const partialCounts = qaDeliveryCounts(context, lifecycleTask);
|
|
4834
|
+
qaExpectBlocked(partial, partialCounts, {
|
|
4835
|
+
message: "ship_it_loop partial lifecycle evidence",
|
|
4836
|
+
missingEvidence: ["mergeability_clean", "manager_merge_decision", "merge", "post_merge_verification", "adversarial_check"],
|
|
4837
|
+
reason: "missing_required_evidence",
|
|
4838
|
+
});
|
|
4839
|
+
checks.push(qaCheck("ship_it_lifecycle_blocks_before_mergeability_and_manager_decision", partial, partialCounts));
|
|
4840
|
+
const artifactDir = qaArtifactDir(context, "ship-it-loop", slug, run.id);
|
|
4841
|
+
const conflictReceipt = join(artifactDir, "conflict-blocked.json");
|
|
4842
|
+
mkdirSync(dirname(conflictReceipt), { recursive: true });
|
|
4843
|
+
const conflictPayload = {
|
|
4844
|
+
conflict_state: "unresolved",
|
|
4845
|
+
max_retries: 2,
|
|
4846
|
+
retry_count: 2,
|
|
4847
|
+
status: "blocked",
|
|
4848
|
+
stop_reason: "conflict_retry_limit_reached",
|
|
4849
|
+
};
|
|
4850
|
+
writeFileSync(conflictReceipt, `${JSON.stringify(sortJson(conflictPayload), null, 2)}\n`);
|
|
4851
|
+
checks.push({
|
|
4852
|
+
artifact_path: conflictReceipt,
|
|
4853
|
+
conflict: conflictPayload,
|
|
4854
|
+
name: "ship_it_conflict_retry_blocks_after_limit",
|
|
4855
|
+
status: "passed",
|
|
4856
|
+
});
|
|
4857
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "mergeability_clean", "ship-it-mergeability-clean", {
|
|
4858
|
+
metadata: { conflicts: false, mergeable_state: "clean" },
|
|
4859
|
+
});
|
|
4860
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "manager_merge_decision", "ship-it-manager-merge-decision", {
|
|
4861
|
+
metadata: { decision: "merge_ready", manager_verified: true },
|
|
4862
|
+
});
|
|
4863
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "merge", "ship-it-merge", {
|
|
4864
|
+
metadata: { merge_sha: "2222222222222222222222222222222222222222" },
|
|
4865
|
+
});
|
|
4866
|
+
qaRecordLoopEvidence(context, lifecycleTask, run.id, "post_merge_verification", "ship-it-post-merge-verification", {
|
|
4867
|
+
metadata: { command: "git rev-parse HEAD && npm test -- --runInBand", status: "pass" },
|
|
4868
|
+
});
|
|
4869
|
+
qaRecordAdversarialEvidence(context, lifecycleTask, run.id, "ship-it-adversarial-proof", {
|
|
4870
|
+
check: "Inspect permission denials, missing-evidence blocks, conflict retry receipt, and final evidence set.",
|
|
4871
|
+
failure_mode: "A ship-it loop could merge after CI green while conflicts, manager decision, or post-merge proof are missing.",
|
|
4872
|
+
result: "Dispatch stayed blocked until mergeability, manager decision, merge, post-merge, and adversarial receipts were present.",
|
|
4873
|
+
});
|
|
4874
|
+
enqueueQaContinue(context, lifecycleTask, run.id, "ship-it-allowed-closeout", "Run ship-it continuation after all lifecycle evidence.");
|
|
4875
|
+
const allowed = qaDispatchContinueOnce(context, "ship-it-allowed-closeout");
|
|
4876
|
+
const allowedCounts = qaDeliveryCounts(context, lifecycleTask);
|
|
4877
|
+
qaExpectDelivered(allowed, allowedCounts, "ship_it_loop allowed closeout");
|
|
4878
|
+
checks.push(qaCheck("ship_it_lifecycle_retry_delivers_after_all_evidence", allowed, allowedCounts));
|
|
4879
|
+
return {
|
|
4880
|
+
artifacts: { conflict_receipt: conflictReceipt, db_path: context.dbPath },
|
|
4881
|
+
checks,
|
|
4882
|
+
generated_at: new Date().toISOString(),
|
|
4883
|
+
generated_tasks: generatedTasks,
|
|
4884
|
+
replay_commands: [
|
|
4885
|
+
"conveyor loop-templates --show ship_it_loop --json",
|
|
4886
|
+
"conveyor manager-recipes --show ship-it-loop --json",
|
|
4887
|
+
"conveyor manager-permission <task> repo.push_branch --require",
|
|
4888
|
+
"conveyor manager-permission <task> repo.open_pr --require",
|
|
4889
|
+
"conveyor manager-permission <task> repo.merge_green_pr --require",
|
|
4890
|
+
"conveyor loop-evidence add <task> --loop-run <run-id> --iteration 1 --evidence-type branch_ready",
|
|
4891
|
+
"conveyor loop-evidence add <task> --loop-run <run-id> --iteration 1 --evidence-type ci_green",
|
|
4892
|
+
"conveyor loop-evidence adversarial-check <task> --loop-run <run-id> --iteration 1 --failure-mode <failure> --check <check> --result <result>",
|
|
4893
|
+
`conveyor dispatch --once --type continue_iteration --dispatcher-id ${context.dispatcherId} --path ${context.dbPath}`,
|
|
4894
|
+
],
|
|
4895
|
+
result: "passed",
|
|
4896
|
+
scenario: "ship-it-loop",
|
|
4897
|
+
template: "ship_it_loop",
|
|
4898
|
+
template_metadata: templateMetadata,
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4647
4901
|
function qaRunAdversarialTriggers(context) {
|
|
4648
4902
|
const slug = randomUUID().slice(0, 8);
|
|
4649
4903
|
const triggerDefinitions = listLoopTriggers();
|
|
@@ -4941,24 +5195,27 @@ function enqueueQaContinue(context, task, runId, correlationId, message) {
|
|
|
4941
5195
|
}
|
|
4942
5196
|
}
|
|
4943
5197
|
function qaDispatchContinueOnce(context, expectedCorrelationId) {
|
|
5198
|
+
return qaDispatchCommandOnce(context, "continue_iteration", expectedCorrelationId);
|
|
5199
|
+
}
|
|
5200
|
+
function qaDispatchCommandOnce(context, commandType, expectedCorrelationId) {
|
|
4944
5201
|
const before = openDatabaseSync(context.dbPath);
|
|
4945
5202
|
try {
|
|
4946
5203
|
initializeDatabaseSync(before);
|
|
4947
5204
|
const rows = before.prepare(`
|
|
4948
5205
|
select correlation_id, state
|
|
4949
5206
|
from commands
|
|
4950
|
-
where type =
|
|
5207
|
+
where type = ? and state in ('pending', 'attempted')
|
|
4951
5208
|
order by created_at, id
|
|
4952
|
-
`).all();
|
|
5209
|
+
`).all(commandType);
|
|
4953
5210
|
const seen = rows.map((row) => `${row.correlation_id}:${row.state}`);
|
|
4954
5211
|
if (rows.length !== 1 || rows[0]?.correlation_id !== expectedCorrelationId || rows[0]?.state !== "pending") {
|
|
4955
|
-
throw new Error(`qa-run
|
|
5212
|
+
throw new Error(`qa-run ${commandType} dispatch queue is not clean; expected only ${expectedCorrelationId}, found ${JSON.stringify(seen)}`);
|
|
4956
5213
|
}
|
|
4957
5214
|
}
|
|
4958
5215
|
finally {
|
|
4959
5216
|
before.close();
|
|
4960
5217
|
}
|
|
4961
|
-
const parsed = parseRuntimeArgs(["dispatch", "--type",
|
|
5218
|
+
const parsed = parseRuntimeArgs(["dispatch", "--type", commandType, "--path", context.dbPath], {
|
|
4962
5219
|
AGENT_CONVEYOR_TS_RUNTIME: "1",
|
|
4963
5220
|
});
|
|
4964
5221
|
const processed = dispatchOncePass(parsed, context.runtimeOptions, {
|
|
@@ -4968,13 +5225,68 @@ function qaDispatchContinueOnce(context, expectedCorrelationId) {
|
|
|
4968
5225
|
limit: 1,
|
|
4969
5226
|
});
|
|
4970
5227
|
if (processed.length !== 1) {
|
|
4971
|
-
throw new Error(`expected exactly one
|
|
5228
|
+
throw new Error(`expected exactly one ${commandType} dispatch item, got ${processed.length}`);
|
|
4972
5229
|
}
|
|
4973
5230
|
if (processed[0]?.correlation_id !== expectedCorrelationId) {
|
|
4974
5231
|
throw new Error(`qa-run dispatched unexpected command ${String(processed[0]?.correlation_id)}`);
|
|
4975
5232
|
}
|
|
4976
5233
|
return processed[0] ?? {};
|
|
4977
5234
|
}
|
|
5235
|
+
function qaConfigureManagerPermissions(context, task, permissions) {
|
|
5236
|
+
const result = runTypescriptRuntimeCommand({
|
|
5237
|
+
...context.runtimeOptions,
|
|
5238
|
+
args: [
|
|
5239
|
+
"manager-config",
|
|
5240
|
+
task.task_name,
|
|
5241
|
+
"--mode",
|
|
5242
|
+
"strict",
|
|
5243
|
+
"--objective",
|
|
5244
|
+
"Ship-it lifecycle QA permission contract.",
|
|
5245
|
+
...permissions.flatMap((permission) => ["--permit", permission]),
|
|
5246
|
+
"--path",
|
|
5247
|
+
context.dbPath,
|
|
5248
|
+
],
|
|
5249
|
+
env: {
|
|
5250
|
+
...(context.runtimeOptions.env ?? {}),
|
|
5251
|
+
AGENT_CONVEYOR_TS_RUNTIME: "1",
|
|
5252
|
+
},
|
|
5253
|
+
});
|
|
5254
|
+
qaRequire(result.exitCode === 0, `manager-config permission setup failed: ${result.stderr ?? result.stdout ?? ""}`);
|
|
5255
|
+
}
|
|
5256
|
+
function qaRunPermissionGate(context, task, options) {
|
|
5257
|
+
const database = openDatabaseSync(context.dbPath);
|
|
5258
|
+
try {
|
|
5259
|
+
initializeDatabaseSync(database);
|
|
5260
|
+
createCommandSync(database, {
|
|
5261
|
+
commandType: "nudge_worker",
|
|
5262
|
+
correlationId: options.correlationId,
|
|
5263
|
+
payload: { message: options.message, ship_it: { required_permission: options.permission } },
|
|
5264
|
+
requiredPermission: options.permission,
|
|
5265
|
+
taskId: task.task_id,
|
|
5266
|
+
});
|
|
5267
|
+
}
|
|
5268
|
+
finally {
|
|
5269
|
+
database.close();
|
|
5270
|
+
}
|
|
5271
|
+
const dispatch = qaDispatchCommandOnce(context, "nudge_worker", options.correlationId);
|
|
5272
|
+
const counts = qaDeliveryCounts(context, task);
|
|
5273
|
+
if (options.expectAllowed === true) {
|
|
5274
|
+
qaExpectDelivered(dispatch, counts, `${options.permission} permission gate`);
|
|
5275
|
+
}
|
|
5276
|
+
else {
|
|
5277
|
+
qaRequire(dispatch.state === "failed", `${options.permission} gate did not fail without permission`);
|
|
5278
|
+
qaRequire(String(dispatch.error ?? "").includes("manager permission required"), `${options.permission} gate failed for the wrong reason`);
|
|
5279
|
+
qaRequire(counts.worker_inbox_count === 0, `${options.permission} denied gate left worker inbox mail`);
|
|
5280
|
+
}
|
|
5281
|
+
return {
|
|
5282
|
+
...counts,
|
|
5283
|
+
command_type: "nudge_worker",
|
|
5284
|
+
dispatch,
|
|
5285
|
+
name: options.checkName,
|
|
5286
|
+
permission: options.permission,
|
|
5287
|
+
status: "passed",
|
|
5288
|
+
};
|
|
5289
|
+
}
|
|
4978
5290
|
function qaDeliveryCounts(context, task) {
|
|
4979
5291
|
const database = openDatabaseSync(context.dbPath);
|
|
4980
5292
|
try {
|
|
@@ -11917,8 +12229,10 @@ const MANAGER_PERMISSION_ACTION_NAMES = new Set([
|
|
|
11917
12229
|
"context.fetch_prs",
|
|
11918
12230
|
"context.spawn_reviewer",
|
|
11919
12231
|
"repo.merge_green_pr",
|
|
12232
|
+
"repo.monitor_ci",
|
|
11920
12233
|
"repo.open_pr",
|
|
11921
12234
|
"repo.push_branch",
|
|
12235
|
+
"repo.resolve_conflicts",
|
|
11922
12236
|
"verification.run_cargo",
|
|
11923
12237
|
"verification.run_playwright",
|
|
11924
12238
|
"verification.run_pytest",
|
|
@@ -14075,6 +14389,7 @@ const DEFERRED_HEADING_RE = /\b(follow[- ]?up|deferred)\b/i;
|
|
|
14075
14389
|
const LIST_ITEM_RE = /^\s*(?:[-*+]|\d+[.)]|\[[ xX]\])\s+(?<text>.+?)\s*$/;
|
|
14076
14390
|
const EMPTY_ITEM_RE = /^(?:n\/?a|none|no follow[- ]?ups?|no deferred(?: criteria)?|nothing)$/i;
|
|
14077
14391
|
const INDENTED_CONTINUATION_RE = /^\s+\S/;
|
|
14392
|
+
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;
|
|
14078
14393
|
function planCriteriaCommands(task, text, options) {
|
|
14079
14394
|
const { suggestions, warnings } = parseWorkerCriteriaResponse(text);
|
|
14080
14395
|
return {
|
|
@@ -14136,6 +14451,11 @@ function parseWorkerCriteriaResponse(text) {
|
|
|
14136
14451
|
else if (suggestions.length === 0) {
|
|
14137
14452
|
warnings.push("Clear criteria headings were found, but no bullet or numbered criteria items were detected.");
|
|
14138
14453
|
}
|
|
14454
|
+
for (const suggestion of suggestions) {
|
|
14455
|
+
if (suggestion.classification?.kind === "manager_closeout_proof") {
|
|
14456
|
+
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.`);
|
|
14457
|
+
}
|
|
14458
|
+
}
|
|
14139
14459
|
return { suggestions, warnings };
|
|
14140
14460
|
}
|
|
14141
14461
|
function headingStatus(line) {
|
|
@@ -14157,12 +14477,23 @@ function makeCriteriaSuggestion(text, status) {
|
|
|
14157
14477
|
return null;
|
|
14158
14478
|
}
|
|
14159
14479
|
return {
|
|
14480
|
+
classification: classifyCriteriaSuggestion(criterion),
|
|
14160
14481
|
criterion,
|
|
14161
14482
|
rationale: status === "deferred" ? DEFAULT_DEFERRED_RATIONALE : null,
|
|
14162
14483
|
source: "worker_proposed",
|
|
14163
14484
|
status,
|
|
14164
14485
|
};
|
|
14165
14486
|
}
|
|
14487
|
+
function classifyCriteriaSuggestion(criterion) {
|
|
14488
|
+
if (!CLOSEOUT_CRITERION_RE.test(criterion)) {
|
|
14489
|
+
return null;
|
|
14490
|
+
}
|
|
14491
|
+
return {
|
|
14492
|
+
kind: "manager_closeout_proof",
|
|
14493
|
+
reason: "The criterion names manager closeout mechanics rather than the worker/task outcome being accepted.",
|
|
14494
|
+
recommendation: "keep_out_of_acceptance_criteria",
|
|
14495
|
+
};
|
|
14496
|
+
}
|
|
14166
14497
|
function suggestionToArgv(task, suggestion, options) {
|
|
14167
14498
|
const argv = [
|
|
14168
14499
|
"conveyor",
|
|
@@ -14698,7 +15029,6 @@ function unsupportedLifecycleTaskOptions(parsed, finish) {
|
|
|
14698
15029
|
|| parsed.flags.includeFullTranscripts
|
|
14699
15030
|
|| parsed.flags.includeLegacy
|
|
14700
15031
|
|| parsed.flags.includeTranscripts
|
|
14701
|
-
|| parsed.flags.json
|
|
14702
15032
|
|| parsed.flags.limit !== null
|
|
14703
15033
|
|| parsed.flags.names.length > 0
|
|
14704
15034
|
|| parsed.flags.output !== null
|
|
@@ -15854,7 +16184,91 @@ const ADVERSARIAL_CHECK_REQUIREMENT = {
|
|
|
15854
16184
|
required: ["failure_mode", "check", "result"],
|
|
15855
16185
|
type: "object",
|
|
15856
16186
|
};
|
|
16187
|
+
const SHIP_IT_ARTIFACT_REQUIREMENTS = {
|
|
16188
|
+
adversarial_check: ADVERSARIAL_CHECK_REQUIREMENT,
|
|
16189
|
+
branch_pushed: {
|
|
16190
|
+
description: "Receipt that the worker branch was pushed only after repo.push_branch was permitted.",
|
|
16191
|
+
properties: {
|
|
16192
|
+
branch: { type: "string" },
|
|
16193
|
+
remote: { type: "string" },
|
|
16194
|
+
},
|
|
16195
|
+
required: ["branch", "remote"],
|
|
16196
|
+
type: "object",
|
|
16197
|
+
},
|
|
16198
|
+
branch_ready: {
|
|
16199
|
+
description: "Branch and commit evidence for the candidate ship-it change.",
|
|
16200
|
+
properties: {
|
|
16201
|
+
branch: { type: "string" },
|
|
16202
|
+
commit_sha: { type: "string" },
|
|
16203
|
+
},
|
|
16204
|
+
required: ["branch", "commit_sha"],
|
|
16205
|
+
type: "object",
|
|
16206
|
+
},
|
|
16207
|
+
ci_green: {
|
|
16208
|
+
description: "Explicit CI/check evidence. Prefer gh pr checks --required, or record why no required checks exist.",
|
|
16209
|
+
properties: {
|
|
16210
|
+
command: { type: "string" },
|
|
16211
|
+
status: { type: "string" },
|
|
16212
|
+
},
|
|
16213
|
+
required: ["command", "status"],
|
|
16214
|
+
type: "object",
|
|
16215
|
+
},
|
|
16216
|
+
manager_merge_decision: {
|
|
16217
|
+
description: "Manager-owned decision that all required evidence has been independently verified and merge is allowed.",
|
|
16218
|
+
properties: {
|
|
16219
|
+
decision: { type: "string" },
|
|
16220
|
+
manager_verified: { type: "boolean" },
|
|
16221
|
+
},
|
|
16222
|
+
required: ["decision", "manager_verified"],
|
|
16223
|
+
type: "object",
|
|
16224
|
+
},
|
|
16225
|
+
merge: {
|
|
16226
|
+
description: "Merge receipt recorded only after repo.merge_green_pr, CI, mergeability, and manager decision gates pass.",
|
|
16227
|
+
properties: {
|
|
16228
|
+
merge_sha: { type: "string" },
|
|
16229
|
+
},
|
|
16230
|
+
required: ["merge_sha"],
|
|
16231
|
+
type: "object",
|
|
16232
|
+
},
|
|
16233
|
+
mergeability_clean: {
|
|
16234
|
+
description: "Evidence that the PR is mergeable or conflicts were resolved within the manager-approved retry limit.",
|
|
16235
|
+
properties: {
|
|
16236
|
+
conflicts: { type: "boolean" },
|
|
16237
|
+
mergeable_state: { type: "string" },
|
|
16238
|
+
},
|
|
16239
|
+
required: ["conflicts", "mergeable_state"],
|
|
16240
|
+
type: "object",
|
|
16241
|
+
},
|
|
16242
|
+
post_merge_verification: {
|
|
16243
|
+
description: "Post-merge or main-branch verification receipt.",
|
|
16244
|
+
properties: {
|
|
16245
|
+
command: { type: "string" },
|
|
16246
|
+
status: { type: "string" },
|
|
16247
|
+
},
|
|
16248
|
+
required: ["command", "status"],
|
|
16249
|
+
type: "object",
|
|
16250
|
+
},
|
|
16251
|
+
pr_url: {
|
|
16252
|
+
description: "Pull request URL recorded only after repo.open_pr was permitted.",
|
|
16253
|
+
properties: {
|
|
16254
|
+
url: { type: "string" },
|
|
16255
|
+
},
|
|
16256
|
+
required: ["url"],
|
|
16257
|
+
type: "object",
|
|
16258
|
+
},
|
|
16259
|
+
};
|
|
15857
16260
|
const LOOP_TEMPLATES = {
|
|
16261
|
+
app_visible_build_loop: {
|
|
16262
|
+
artifactRequirements: { adversarial_check: ADVERSARIAL_CHECK_REQUIREMENT },
|
|
16263
|
+
cleanupPolicy: "off",
|
|
16264
|
+
description: "Require build evidence and adversarial proof between visible Codex app iterations without a cleanup gate.",
|
|
16265
|
+
maxIterations: 2,
|
|
16266
|
+
name: "app_visible_build_loop",
|
|
16267
|
+
recommendedTools: ["verification.run_tests"],
|
|
16268
|
+
requiredBeforeContinue: ["build_passed", "adversarial_check"],
|
|
16269
|
+
stopConditions: ["max_iterations", "required_evidence"],
|
|
16270
|
+
tags: ["build", "codex_app", "visible_session"],
|
|
16271
|
+
},
|
|
15858
16272
|
build_then_clear: {
|
|
15859
16273
|
artifactRequirements: {},
|
|
15860
16274
|
cleanupPolicy: "clear",
|
|
@@ -15888,6 +16302,27 @@ const LOOP_TEMPLATES = {
|
|
|
15888
16302
|
stopConditions: ["max_iterations", "required_evidence"],
|
|
15889
16303
|
tags: ["repo", "ci"],
|
|
15890
16304
|
},
|
|
16305
|
+
ship_it_loop: {
|
|
16306
|
+
artifactRequirements: SHIP_IT_ARTIFACT_REQUIREMENTS,
|
|
16307
|
+
cleanupPolicy: "clear",
|
|
16308
|
+
description: "Require branch, push, PR, CI, mergeability, manager merge decision, merge, post-merge, and adversarial evidence before ship-it continuation.",
|
|
16309
|
+
maxIterations: 2,
|
|
16310
|
+
name: "ship_it_loop",
|
|
16311
|
+
recommendedTools: ["gh", "verification.run_tests", "git"],
|
|
16312
|
+
requiredBeforeContinue: [
|
|
16313
|
+
"branch_ready",
|
|
16314
|
+
"branch_pushed",
|
|
16315
|
+
"pr_url",
|
|
16316
|
+
"ci_green",
|
|
16317
|
+
"mergeability_clean",
|
|
16318
|
+
"manager_merge_decision",
|
|
16319
|
+
"merge",
|
|
16320
|
+
"post_merge_verification",
|
|
16321
|
+
"adversarial_check",
|
|
16322
|
+
],
|
|
16323
|
+
stopConditions: ["max_iterations", "required_evidence", "manager_accepts"],
|
|
16324
|
+
tags: ["repo", "ci", "merge", "ship_it"],
|
|
16325
|
+
},
|
|
15891
16326
|
test_coverage_loop: {
|
|
15892
16327
|
artifactRequirements: { adversarial_check: ADVERSARIAL_CHECK_REQUIREMENT },
|
|
15893
16328
|
cleanupPolicy: "clear",
|
|
@@ -16010,6 +16445,9 @@ const MANAGER_RECIPES = {
|
|
|
16010
16445
|
"PR/CI/merge or satisfied_on_main proof",
|
|
16011
16446
|
"parent receipt update before the next child",
|
|
16012
16447
|
],
|
|
16448
|
+
finalReportRequirements: [
|
|
16449
|
+
"Record manager closeout proof, including final task state and any finish-task/heartbeat teardown receipt, in the final report instead of accepted worker criteria.",
|
|
16450
|
+
],
|
|
16013
16451
|
guidelines: [
|
|
16014
16452
|
"Keep exactly one child board active at a time.",
|
|
16015
16453
|
"Before activating the next child, update the parent receipt.",
|
|
@@ -16033,6 +16471,9 @@ const MANAGER_RECIPES = {
|
|
|
16033
16471
|
displayName: "Nudge / What's Next Manager",
|
|
16034
16472
|
epilogues: [],
|
|
16035
16473
|
evidenceGates: ["manager decision", "worker receipt", "accepted criteria closure"],
|
|
16474
|
+
finalReportRequirements: [
|
|
16475
|
+
"Record status, residual risk, and any finish-task or terminal closeout proof in the final report, not as worker acceptance criteria.",
|
|
16476
|
+
],
|
|
16036
16477
|
guidelines: [
|
|
16037
16478
|
"Prefer wait over nudge while the worker is active.",
|
|
16038
16479
|
"Ask for must-have current-task criteria versus follow-ups when scope changes.",
|
|
@@ -16060,6 +16501,9 @@ const MANAGER_RECIPES = {
|
|
|
16060
16501
|
displayName: "PR/CI/Merge Ralph Loop",
|
|
16061
16502
|
epilogues: ["draft-pr", "record-handoff"],
|
|
16062
16503
|
evidenceGates: ["pr_url", "ci_green", "merge", "adversarial_check"],
|
|
16504
|
+
finalReportRequirements: [
|
|
16505
|
+
"Record PR URL, CI, merge, handoff, finish-task, and cleanup receipts in the final report; keep accepted criteria focused on deliverable proof.",
|
|
16506
|
+
],
|
|
16063
16507
|
guidelines: ["Merge only after green CI and recorded manager decision evidence."],
|
|
16064
16508
|
loopTemplate: "pr_ci_merge_loop",
|
|
16065
16509
|
mode: "strict",
|
|
@@ -16069,6 +16513,57 @@ const MANAGER_RECIPES = {
|
|
|
16069
16513
|
supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
|
|
16070
16514
|
tools: ["verification.run_tests", "context.fetch_prs"],
|
|
16071
16515
|
},
|
|
16516
|
+
"ship-it-loop": {
|
|
16517
|
+
acceptance: [
|
|
16518
|
+
"Branch, push, PR URL, CI-green, mergeability, manager merge decision, merge, post-merge verification, and adversarial proof are recorded.",
|
|
16519
|
+
"Push, PR creation, conflict resolution, and merge actions are each gated by explicit manager permissions.",
|
|
16520
|
+
"Merge readiness is a manager decision after independent verification, not a worker claim or CI-green shortcut.",
|
|
16521
|
+
],
|
|
16522
|
+
cleanup: "clear after saved handoff",
|
|
16523
|
+
description: "Drive a visible manager-worker ship-it loop through branch push, PR, CI, conflict handling, manager merge decision, merge, and post-merge receipts.",
|
|
16524
|
+
disallowedActions: [
|
|
16525
|
+
"Do not push branches before repo.push_branch is permitted.",
|
|
16526
|
+
"Do not open or update PRs before repo.open_pr is permitted.",
|
|
16527
|
+
"Do not resolve conflicts before repo.resolve_conflicts is permitted and retry bounds are recorded.",
|
|
16528
|
+
"Do not merge before repo.merge_green_pr is permitted, CI is green, mergeability is clean, and the manager records merge_ready.",
|
|
16529
|
+
],
|
|
16530
|
+
displayName: "Autonomous Ship-It Loop",
|
|
16531
|
+
epilogues: ["draft-pr", "record-handoff"],
|
|
16532
|
+
evidenceGates: [
|
|
16533
|
+
"branch_ready",
|
|
16534
|
+
"branch_pushed",
|
|
16535
|
+
"pr_url",
|
|
16536
|
+
"ci_green",
|
|
16537
|
+
"mergeability_clean",
|
|
16538
|
+
"manager_merge_decision",
|
|
16539
|
+
"merge",
|
|
16540
|
+
"post_merge_verification",
|
|
16541
|
+
"adversarial_check",
|
|
16542
|
+
],
|
|
16543
|
+
finalReportRequirements: [
|
|
16544
|
+
"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.",
|
|
16545
|
+
],
|
|
16546
|
+
guidelines: [
|
|
16547
|
+
"Keep all PR lifecycle phases visible in the manager and worker sessions.",
|
|
16548
|
+
"Treat CI-green, mergeability, and worker receipts as claims until the manager verifies them.",
|
|
16549
|
+
"Use a bounded conflict retry and block with evidence when conflicts remain unresolved.",
|
|
16550
|
+
],
|
|
16551
|
+
loopTemplate: "ship_it_loop",
|
|
16552
|
+
mode: "strict",
|
|
16553
|
+
name: "ship-it-loop",
|
|
16554
|
+
objective: "Supervise a worker from implementation through explicit branch, PR, CI, conflict, merge, and post-merge evidence gates.",
|
|
16555
|
+
permissions: [
|
|
16556
|
+
"repo.push_branch",
|
|
16557
|
+
"repo.open_pr",
|
|
16558
|
+
"repo.monitor_ci",
|
|
16559
|
+
"repo.resolve_conflicts",
|
|
16560
|
+
"repo.merge_green_pr",
|
|
16561
|
+
"worker_session.compact",
|
|
16562
|
+
"worker_session.clear",
|
|
16563
|
+
],
|
|
16564
|
+
supportPatterns: ["Inbox / No-Tmux App Loop", "Recovery / Resume / Handoff"],
|
|
16565
|
+
tools: ["gh", "git", "verification.run_tests", "context.fetch_prs"],
|
|
16566
|
+
},
|
|
16072
16567
|
"test-coverage-loop": {
|
|
16073
16568
|
acceptance: [
|
|
16074
16569
|
"Coverage or targeted test evidence is recorded before another worker pass.",
|
|
@@ -16080,6 +16575,9 @@ const MANAGER_RECIPES = {
|
|
|
16080
16575
|
displayName: "Test Coverage Loop",
|
|
16081
16576
|
epilogues: [],
|
|
16082
16577
|
evidenceGates: ["test_coverage", "adversarial_check"],
|
|
16578
|
+
finalReportRequirements: [
|
|
16579
|
+
"Record final closeout and finish-task proof in the manager final report; do not make closeout mechanics a test-coverage criterion.",
|
|
16580
|
+
],
|
|
16083
16581
|
guidelines: ["Record coverage evidence before asking for another worker pass."],
|
|
16084
16582
|
loopTemplate: "test_coverage_loop",
|
|
16085
16583
|
mode: "strict",
|
|
@@ -16106,6 +16604,9 @@ const MANAGER_RECIPES = {
|
|
|
16106
16604
|
"diff_below_threshold",
|
|
16107
16605
|
"adversarial_check",
|
|
16108
16606
|
],
|
|
16607
|
+
finalReportRequirements: [
|
|
16608
|
+
"Record final visual decision, closeout, and cleanup proof in the manager final report; keep accepted criteria focused on visible-output evidence.",
|
|
16609
|
+
],
|
|
16109
16610
|
guidelines: ["Compare visible output against references before requesting another pass."],
|
|
16110
16611
|
loopTemplate: "visual_diff_loop",
|
|
16111
16612
|
mode: "guided",
|
|
@@ -16124,6 +16625,9 @@ const MANAGER_RECIPE_ALIASES = {
|
|
|
16124
16625
|
"pr ci merge ralph loop": "pr-ci-merge-ralph-loop",
|
|
16125
16626
|
"pr/ci/merge ralph loop": "pr-ci-merge-ralph-loop",
|
|
16126
16627
|
"ralph loop": "pr-ci-merge-ralph-loop",
|
|
16628
|
+
"ship it": "ship-it-loop",
|
|
16629
|
+
"ship it loop": "ship-it-loop",
|
|
16630
|
+
"ship-it": "ship-it-loop",
|
|
16127
16631
|
"test coverage": "test-coverage-loop",
|
|
16128
16632
|
"test coverage loop": "test-coverage-loop",
|
|
16129
16633
|
"ux polish": "ux-polish-loop",
|
|
@@ -16157,6 +16661,7 @@ function managerRecipeSummary(name) {
|
|
|
16157
16661
|
display_name: recipe.displayName,
|
|
16158
16662
|
epilogues: [...recipe.epilogues],
|
|
16159
16663
|
evidence_gates: [...recipe.evidenceGates],
|
|
16664
|
+
final_report_requirements: [...recipe.finalReportRequirements],
|
|
16160
16665
|
guidelines: [...recipe.guidelines],
|
|
16161
16666
|
locked_summary_template: lockedManagerRecipeSummary(recipe),
|
|
16162
16667
|
loop_template: recipe.loopTemplate,
|
|
@@ -16211,6 +16716,7 @@ function lockedManagerRecipeSummary(recipe) {
|
|
|
16211
16716
|
`Epilogues: ${recipe.epilogues.length > 0 ? recipe.epilogues.join(", ") : "none"}`,
|
|
16212
16717
|
`Cleanup: ${recipe.cleanup}`,
|
|
16213
16718
|
`Evidence gates: ${recipe.evidenceGates.length > 0 ? recipe.evidenceGates.join(", ") : "manager-reviewed evidence"}`,
|
|
16719
|
+
`Final report: ${recipe.finalReportRequirements.join("; ")}`,
|
|
16214
16720
|
`Not allowed: ${recipe.disallowedActions.length > 0 ? recipe.disallowedActions.join("; ") : "unconfirmed custom actions"}`,
|
|
16215
16721
|
"User confirmed: <yes|no>",
|
|
16216
16722
|
].join("\n");
|
|
@@ -16517,7 +17023,7 @@ function loopStatusSummarySync(database, options) {
|
|
|
16517
17023
|
const matchingCommands = commandRows.filter((row) => commandRowMatchesRun(row, options.run.id));
|
|
16518
17024
|
const commandStates = countBy(matchingCommands.map((row) => row.state));
|
|
16519
17025
|
const notificationRows = database.prepare(`
|
|
16520
|
-
select state, payload_json
|
|
17026
|
+
select consumed_at, state, payload_json
|
|
16521
17027
|
from routed_notifications
|
|
16522
17028
|
where task_id = ?
|
|
16523
17029
|
order by created_at, id
|
|
@@ -16550,6 +17056,12 @@ function loopStatusSummarySync(database, options) {
|
|
|
16550
17056
|
.filter((value) => typeof value === "string" && value.length > 0))].sort();
|
|
16551
17057
|
const telemetryEvents = telemetryEventsForRunSync(database, { runId: options.run.id, taskId: options.task.id });
|
|
16552
17058
|
const telemetryByType = countBy(telemetryEvents.map((event) => event.event_type));
|
|
17059
|
+
const appTaskDispatch = appTaskDispatchSummarySync(database, {
|
|
17060
|
+
commandRows,
|
|
17061
|
+
notificationRows,
|
|
17062
|
+
runScopedActivityTotal: matchingCommands.length + matchingNotifications.length + telemetryEvents.length,
|
|
17063
|
+
taskId: options.task.id,
|
|
17064
|
+
});
|
|
16553
17065
|
const failedCommandCount = commandStates.failed ?? 0;
|
|
16554
17066
|
const failureCounts = loopFailureCountsSync(database, {
|
|
16555
17067
|
failedCommandCount,
|
|
@@ -16570,6 +17082,7 @@ function loopStatusSummarySync(database, options) {
|
|
|
16570
17082
|
total: evidenceItems.length,
|
|
16571
17083
|
types: evidenceTypes,
|
|
16572
17084
|
},
|
|
17085
|
+
app_task_dispatch: appTaskDispatch,
|
|
16573
17086
|
failures: failureCounts,
|
|
16574
17087
|
inbox: {
|
|
16575
17088
|
worker_unconsumed: workerInbox,
|
|
@@ -16613,6 +17126,52 @@ function telemetryEventsForRunSync(database, options) {
|
|
|
16613
17126
|
limit 1000
|
|
16614
17127
|
`).all(options.taskId, options.runId);
|
|
16615
17128
|
}
|
|
17129
|
+
function appTaskDispatchSummarySync(database, options) {
|
|
17130
|
+
const taskDispatchEventTypes = [
|
|
17131
|
+
"app_autopilot_started",
|
|
17132
|
+
"app_autopilot_stopped",
|
|
17133
|
+
"app_heartbeat",
|
|
17134
|
+
"app_wakeup_delivery_recorded",
|
|
17135
|
+
"app_wakeup_dispatch_planned",
|
|
17136
|
+
"command_created",
|
|
17137
|
+
"dispatch_inbox_consumed",
|
|
17138
|
+
];
|
|
17139
|
+
const telemetryRows = database.prepare(`
|
|
17140
|
+
select event_type, timestamp
|
|
17141
|
+
from telemetry_events
|
|
17142
|
+
where task_id = ?
|
|
17143
|
+
and event_type in (${taskDispatchEventTypes.map(() => "?").join(", ")})
|
|
17144
|
+
order by timestamp, id
|
|
17145
|
+
`).all(options.taskId, ...taskDispatchEventTypes);
|
|
17146
|
+
const telemetryByType = countBy(telemetryRows.map((row) => row.event_type));
|
|
17147
|
+
const commandStates = countBy(options.commandRows.map((row) => row.state));
|
|
17148
|
+
const notificationStates = countBy(options.notificationRows.map((row) => row.state));
|
|
17149
|
+
const recordsTotal = options.commandRows.length + options.notificationRows.length + telemetryRows.length;
|
|
17150
|
+
const blindToRun = options.runScopedActivityTotal === 0 && recordsTotal > 0;
|
|
17151
|
+
return {
|
|
17152
|
+
commands: {
|
|
17153
|
+
states: sortJson(commandStates),
|
|
17154
|
+
total: options.commandRows.length,
|
|
17155
|
+
},
|
|
17156
|
+
latest_event_at: telemetryRows.at(-1)?.timestamp ?? null,
|
|
17157
|
+
note: blindToRun
|
|
17158
|
+
? "Requested run has no run-scoped activity, but task-level app Dispatch records exist."
|
|
17159
|
+
: null,
|
|
17160
|
+
notifications: {
|
|
17161
|
+
delivered_unconsumed: options.notificationRows
|
|
17162
|
+
.filter((row) => row.state === "delivered" && row.consumed_at === null).length,
|
|
17163
|
+
states: sortJson(notificationStates),
|
|
17164
|
+
total: options.notificationRows.length,
|
|
17165
|
+
},
|
|
17166
|
+
records_total: recordsTotal,
|
|
17167
|
+
telemetry: {
|
|
17168
|
+
by_event_type: sortJson(telemetryByType),
|
|
17169
|
+
command_created: telemetryByType.command_created ?? 0,
|
|
17170
|
+
dispatch_inbox_consumed: telemetryByType.dispatch_inbox_consumed ?? 0,
|
|
17171
|
+
total: telemetryRows.length,
|
|
17172
|
+
},
|
|
17173
|
+
};
|
|
17174
|
+
}
|
|
16616
17175
|
function loopFailureCountsSync(database, options) {
|
|
16617
17176
|
const failedCycles = database.prepare(`
|
|
16618
17177
|
select count(distinct mc.id) as count
|
|
@@ -16702,6 +17261,8 @@ function renderLoopStatusText(result) {
|
|
|
16702
17261
|
const notifications = result.notifications;
|
|
16703
17262
|
const inbox = result.inbox;
|
|
16704
17263
|
const telemetry = result.telemetry;
|
|
17264
|
+
const appTaskDispatch = result.app_task_dispatch;
|
|
17265
|
+
const appTaskDispatchTelemetry = appTaskDispatch?.telemetry;
|
|
16705
17266
|
return [
|
|
16706
17267
|
`task: ${task.name} (${task.state})`,
|
|
16707
17268
|
`run: ${run.name || run.id} (${run.status})`,
|
|
@@ -16710,6 +17271,7 @@ function renderLoopStatusText(result) {
|
|
|
16710
17271
|
`notifications: ${notifications.delivered}/${notifications.total} delivered`,
|
|
16711
17272
|
`worker_unconsumed: ${inbox.worker_unconsumed}`,
|
|
16712
17273
|
`dispatch_inbox_consumed: ${telemetry.dispatch_inbox_consumed}`,
|
|
17274
|
+
`app_task_dispatch: ${appTaskDispatch?.records_total ?? 0} records ${JSON.stringify(appTaskDispatchTelemetry?.by_event_type ?? {})}${appTaskDispatch?.note ? ` (${appTaskDispatch.note})` : ""}`,
|
|
16713
17275
|
`failures: ${JSON.stringify(result.failures ?? {})}`,
|
|
16714
17276
|
`recommendation: ${result.recommendation}`,
|
|
16715
17277
|
].join("\n") + "\n";
|
|
@@ -17218,11 +17780,18 @@ function disposableWorkerHandoff(taskName, runName, dbPath) {
|
|
|
17218
17780
|
const loopClause = runName
|
|
17219
17781
|
? ` for Ralph loop run ${runName}`
|
|
17220
17782
|
: " for this disposable no-tmux binding";
|
|
17783
|
+
const notifyCommand = durableWorkerNotifyManagerCommand(taskName, dbPath);
|
|
17784
|
+
const dispatchCommand = durableWorkerNotifyDispatchCommand(dbPath);
|
|
17221
17785
|
return [
|
|
17222
17786
|
"Use the manage-codex-workers skill.",
|
|
17223
17787
|
"",
|
|
17224
17788
|
`You are the worker for task ${taskName}${loopClause}.`,
|
|
17225
17789
|
"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.",
|
|
17790
|
+
...visibleSessionProtocolLines("worker"),
|
|
17791
|
+
"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.",
|
|
17792
|
+
`Run: ${notifyCommand}`,
|
|
17793
|
+
`Then run: ${dispatchCommand}`,
|
|
17794
|
+
"If either notify/dispatch command fails, include that failure as the blocker and do not claim the manager was notified.",
|
|
17226
17795
|
"",
|
|
17227
17796
|
"Because this is a pull-required Codex app/no-tmux session, autonomous operation requires a heartbeat/wake layer that repeats this worker inbox poll while the thread is idle. If no heartbeat automation is available, report the loop as manual-poll only.",
|
|
17228
17797
|
"Do not delete, pause, or disable heartbeat automation just because an inbox poll is idle; the manager or operator owns terminal loop teardown.",
|
|
@@ -17263,6 +17832,8 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
17263
17832
|
const workerHeartbeatCommand = disposableAppHeartbeatCommand("worker", taskName, dbPath);
|
|
17264
17833
|
const managerInboxCommand = sessionPollCommand("manager", taskName, dbPath);
|
|
17265
17834
|
const workerInboxCommand = sessionPollCommand("worker", taskName, dbPath);
|
|
17835
|
+
const workerNotifyCommand = durableWorkerNotifyManagerCommand(taskName, dbPath);
|
|
17836
|
+
const workerNotifyDispatchCommand = durableWorkerNotifyDispatchCommand(dbPath);
|
|
17266
17837
|
const wakeupDispatchCommand = `${conveyorPollInvocation()} app-wakeup-dispatch ${shellQuote(taskName)} --path ${shellQuote(dbPath)} --json`;
|
|
17267
17838
|
const deliveryReceiptCommands = disposableDeliveryReceiptCommands(taskName, dbPath);
|
|
17268
17839
|
return {
|
|
@@ -17299,9 +17870,11 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
17299
17870
|
`After a successful app-thread send, record it with: ${deliveryReceiptCommands.sent}`,
|
|
17300
17871
|
`For healthy skipped actions, record: ${deliveryReceiptCommands.skipped}`,
|
|
17301
17872
|
`For missing-thread blocked actions, record: ${deliveryReceiptCommands.blocked}`,
|
|
17873
|
+
...visibleSessionProtocolLines("manager"),
|
|
17302
17874
|
"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.",
|
|
17303
17875
|
"If no item is consumed, stop after a one-line idle receipt.",
|
|
17304
17876
|
"Do not delete, pause, or disable manager or worker heartbeat automation after an idle poll; an idle poll is only a quiet interval.",
|
|
17877
|
+
"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.",
|
|
17305
17878
|
`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}`,
|
|
17306
17879
|
"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.",
|
|
17307
17880
|
].join("\n"),
|
|
@@ -17315,7 +17888,12 @@ function disposableHeartbeatRecommendations(taskName, dbPath) {
|
|
|
17315
17888
|
`Run the worker app heartbeat for task ${taskName}.`,
|
|
17316
17889
|
`Run: ${workerHeartbeatCommand}`,
|
|
17317
17890
|
`If the heartbeat output asks for direct inbox polling, run: ${workerInboxCommand}`,
|
|
17891
|
+
...visibleSessionProtocolLines("worker"),
|
|
17318
17892
|
"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.",
|
|
17893
|
+
"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.",
|
|
17894
|
+
`Run: ${workerNotifyCommand}`,
|
|
17895
|
+
`Then run: ${workerNotifyDispatchCommand}`,
|
|
17896
|
+
"If either notify/dispatch command fails, include that failure as the blocker and do not claim the manager was notified.",
|
|
17319
17897
|
"If no item is consumed, stop after a one-line idle receipt.",
|
|
17320
17898
|
"Do not delete, pause, or disable worker heartbeat automation after an idle poll; the manager or operator owns terminal loop teardown.",
|
|
17321
17899
|
].join("\n"),
|
|
@@ -17335,6 +17913,12 @@ function disposableDeliveryReceiptCommands(taskName, dbPath) {
|
|
|
17335
17913
|
function disposableAppHeartbeatCommand(role, taskName, dbPath) {
|
|
17336
17914
|
return `${conveyorPollInvocation()} app-heartbeat ${shellQuote(taskName)} --role ${role} --path ${shellQuote(dbPath)} --json`;
|
|
17337
17915
|
}
|
|
17916
|
+
function durableWorkerNotifyManagerCommand(taskName, dbPath) {
|
|
17917
|
+
return `${conveyorPollInvocation()} enqueue-notify-manager ${shellQuote(taskName)} --message ${shellQuote("<compact completion/blocker report with files, commands, evidence, residual risk, and next recommended worker task>")} --correlation-id ${shellQuote("<worker-result-id>")} --path ${shellQuote(dbPath)} --json`;
|
|
17918
|
+
}
|
|
17919
|
+
function durableWorkerNotifyDispatchCommand(dbPath) {
|
|
17920
|
+
return `${conveyorPollInvocation()} dispatch --watch --watch-iterations 1 --interval 2 --dispatcher-id dispatch-local --path ${shellQuote(dbPath)} --json`;
|
|
17921
|
+
}
|
|
17338
17922
|
function sessionPollCommand(role, taskName, dbPath) {
|
|
17339
17923
|
const inbox = role === "worker" ? "worker-inbox" : "manager-inbox";
|
|
17340
17924
|
const task = taskName ? shellQuote(taskName) : "<task>";
|
|
@@ -17540,6 +18124,7 @@ function startManagerBootstrapPrompt(database, options) {
|
|
|
17540
18124
|
"- Treat acceptance criteria as living supervision state.",
|
|
17541
18125
|
"- Inspect `manager_context.acceptance_criteria` each cycle.",
|
|
17542
18126
|
"- If worker progress reveals new edge cases, tests, polish, or scope boundaries, ask the worker to propose must-have vs follow-up criteria.",
|
|
18127
|
+
"- 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.",
|
|
17543
18128
|
"- Before finishing, compare worker receipts/verification against accepted open criteria.",
|
|
17544
18129
|
`- For each accepted criterion that is proven, record evidence with \`${satisfyCriterionCommand}\`.`,
|
|
17545
18130
|
`- 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}\`.`,
|