@tea-agent/loop-agent 0.28.1 → 0.28.2

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.
Files changed (66) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +11 -1
  4. package/dist/cli/command-definitions.js +2 -1
  5. package/dist/commands/client-recovery.js +111 -8
  6. package/dist/commands/dag-init-hybrid.js +1 -1
  7. package/dist/commands/init-upgrade.js +2479 -0
  8. package/dist/commands/init.js +120 -9
  9. package/dist/governance/manifest-types.js +65 -0
  10. package/dist/shared/operator/capabilities.js +350 -2
  11. package/dist/task/worktree.js +256 -39
  12. package/dist/worker/cli.js +22 -12
  13. package/dist/worker/console/chat/workspace-landing.js +16 -6
  14. package/dist/worker/console/observe-health-match.js +2 -0
  15. package/dist/worker/console/observe-link.js +4 -0
  16. package/dist/worker/console/operator-actions.js +183 -4
  17. package/dist/worker/console/operator-selection.js +13 -0
  18. package/dist/worker/console/static/assets/index-BfRgtLF4.js +29 -0
  19. package/dist/worker/console/static/index.html +1 -1
  20. package/dist/worker/observe/health.js +1 -0
  21. package/dist/worker/observe/night-jobs.js +104 -0
  22. package/dist/worker/observe/routes.js +48 -0
  23. package/dist/worker/observe/static/app.js +3 -0
  24. package/dist/worker/observe/static/constants.js +1 -0
  25. package/dist/worker/observe/static/index.html +47 -0
  26. package/dist/worker/observe/static/router.js +10 -0
  27. package/dist/worker/observe/static/shell-chrome.js +1 -0
  28. package/dist/worker/observe/static/views/night.js +201 -0
  29. package/dist/worker/report/morning-report.js +56 -16
  30. package/dist/worker/run-task/execute-prepared-task.js +153 -0
  31. package/dist/worker/runner/single-task-attempt.js +147 -0
  32. package/dist/worker/scheduler/admission.js +536 -0
  33. package/dist/worker/scheduler/auto-followup.js +99 -0
  34. package/dist/worker/scheduler/cli.js +539 -0
  35. package/dist/worker/scheduler/dispatcher.js +503 -0
  36. package/dist/worker/scheduler/doctor.js +346 -0
  37. package/dist/worker/scheduler/evidence.js +170 -0
  38. package/dist/worker/scheduler/git-base.js +52 -0
  39. package/dist/worker/scheduler/index.js +23 -0
  40. package/dist/worker/scheduler/lease.js +114 -0
  41. package/dist/worker/scheduler/lifecycle.js +348 -0
  42. package/dist/worker/scheduler/lock.js +80 -0
  43. package/dist/worker/scheduler/morning-window.js +161 -0
  44. package/dist/worker/scheduler/night-git-finalizer.js +88 -0
  45. package/dist/worker/scheduler/night-harvest.js +421 -0
  46. package/dist/worker/scheduler/paths.js +84 -0
  47. package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
  48. package/dist/worker/scheduler/recovery.js +277 -0
  49. package/dist/worker/scheduler/reservation.js +146 -0
  50. package/dist/worker/scheduler/retry.js +199 -0
  51. package/dist/worker/scheduler/scheduler-loop.js +272 -0
  52. package/dist/worker/scheduler/store.js +275 -0
  53. package/dist/worker/scheduler/traceability.js +54 -0
  54. package/dist/worker/scheduler/trigger.js +258 -0
  55. package/dist/worker/scheduler/types.js +369 -0
  56. package/dist/worker/scheduler/workspace-adapter.js +91 -0
  57. package/docs/architecture/runtime-boundaries.md +9 -0
  58. package/docs/init-surface.manifest.json +9 -2
  59. package/docs/templates/harness.schema.json +107 -0
  60. package/docs/templates/init-managed-agents.md +18 -8
  61. package/harness.json +22 -0
  62. package/package.json +1 -1
  63. package/skills/loop-agent/SKILL.md +28 -36
  64. package/skills/loop-agent/references/command-reference.md +40 -16
  65. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  66. package/dist/worker/console/static/assets/index-CNO7n6qB.js +0 -29
@@ -6,7 +6,7 @@ import { isDeepStrictEqual } from "node:util";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
8
8
  import { loadHarnessManifest } from "../governance/harness.js";
9
- import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, applyPiRetryMerge, buildOpenCodeContextOverflowCompactPluginSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectPiRetryConfig, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
9
+ import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, PI_PROJECT_SETTINGS_PATH, applyPiRetryMerge, applyProjectPiSettingsMerge, buildOpenCodeContextOverflowCompactPluginSource, buildProjectPiSettingsSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectProjectPiSettings, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
10
10
  const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
11
11
  const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
12
12
  const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
@@ -879,6 +879,7 @@ export function buildManagedGitignoreBlock() {
879
879
  ".harness/cache/",
880
880
  ".harness/*.log",
881
881
  ".harness/init-surface.json",
882
+ ".harness/init-upgrades/",
882
883
  "",
883
884
  "# Eval Lab runtime (candidates, campaigns, scorecards, dogfood scratch)",
884
885
  ".harness/evaluation/",
@@ -1245,6 +1246,7 @@ function inferInitSurfaceMode(relativePath) {
1245
1246
  relativePath === OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH ||
1246
1247
  relativePath === OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH ||
1247
1248
  relativePath === PI_CONTEXT_OVERFLOW_EXTENSION_PATH ||
1249
+ relativePath === PI_PROJECT_SETTINGS_PATH ||
1248
1250
  relativePath.startsWith("scripts/") ||
1249
1251
  relativePath.startsWith(".harness/prompts/") ||
1250
1252
  relativePath.startsWith("docs/README.md") ||
@@ -1301,6 +1303,9 @@ async function buildDesiredSurfaceContent(input) {
1301
1303
  if (manifestPath === PI_CONTEXT_OVERFLOW_EXTENSION_PATH) {
1302
1304
  return { content: buildPiContextOverflowExtensionSource() };
1303
1305
  }
1306
+ if (manifestPath === PI_PROJECT_SETTINGS_PATH) {
1307
+ return { content: buildProjectPiSettingsSource() };
1308
+ }
1304
1309
  if (manifestPath.startsWith("scripts/")) {
1305
1310
  const scripts = buildInitScriptFiles(input.governanceRoot);
1306
1311
  return { content: scripts[manifestPath] };
@@ -1488,13 +1493,17 @@ async function writeInitSurfaceState(input) {
1488
1493
  for (const [pathName, file] of Object.entries(state.files)) {
1489
1494
  if (file.relationship !== "local-existing-unknown")
1490
1495
  continue;
1491
- const priorHash = input.preserveOwnershipFrom.files[pathName]?.currentSha256;
1496
+ const priorFile = input.preserveOwnershipFrom.files[pathName];
1497
+ const priorHash = priorFile?.currentSha256;
1492
1498
  if (priorHash !== undefined) {
1493
1499
  file.currentSha256 = priorHash;
1494
1500
  }
1495
1501
  else {
1496
1502
  delete file.currentSha256;
1497
1503
  }
1504
+ if (priorFile?.acceptedMerge) {
1505
+ file.acceptedMerge = { ...priorFile.acceptedMerge };
1506
+ }
1498
1507
  }
1499
1508
  }
1500
1509
  const target = path.join(input.repoRoot, INIT_SURFACE_STATE_PATH);
@@ -2267,6 +2276,12 @@ export function buildInitInstructions(input) {
2267
2276
  "- Confirm provider/model. Defaults may be used, but the target project may need a different provider/model matrix.",
2268
2277
  "- Default: merge existing AGENTS.md, harness.json, and loop-agent governance docs instead of overwriting user content.",
2269
2278
  "",
2279
+ "## Initialization Upgrade Routing",
2280
+ "",
2281
+ "When the user says `loop-agent初始化更新`, `loop-agent 初始化更新`, `更新 loop-agent 初始化内容`, or an equivalent write request, run `loop-agent init upgrade --repo-root . --json` as the single controller-owned entry. Complete returned single-file allowedPaths merge tasks and call `--continue` until a stable terminal result; do not stop at check-update, needs-safe-update, needs-model-merge, or verification-pending.",
2282
+ "Explicit `检查初始化更新`, `初始化更新校验`, or `只检查,不要修改` stays strictly read-only: run only `loop-agent init check-update --repo-root . --markdown` and do not create an upgrade run.",
2283
+ "Upgrade manages project `.opencode/plugins/`, `.pi/extensions/`, and `.pi/settings.json` by nested merge. Pi must trust the project before loading these settings; do not read or write `~/.pi/agent/settings.json` by default.",
2284
+ "",
2270
2285
  "## Apply Defaults",
2271
2286
  "",
2272
2287
  '- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: "loop-agent"`.',
@@ -2477,6 +2492,7 @@ export async function initializeLoopAgentProject(options) {
2477
2492
  OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
2478
2493
  OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH,
2479
2494
  PI_CONTEXT_OVERFLOW_EXTENSION_PATH,
2495
+ PI_PROJECT_SETTINGS_PATH,
2480
2496
  ];
2481
2497
  if (clientRecoveryMode === "off") {
2482
2498
  for (const recoveryPath of clientRecoveryPaths)
@@ -2487,6 +2503,7 @@ export async function initializeLoopAgentProject(options) {
2487
2503
  clientRecovery.plugin,
2488
2504
  clientRecovery.overflowPlugin,
2489
2505
  clientRecovery.piExtension,
2506
+ { written: clientRecovery.pi?.wrote === true },
2490
2507
  ];
2491
2508
  for (const [index, recoveryPath] of clientRecoveryPaths.entries()) {
2492
2509
  if (installResults[index]?.written)
@@ -2540,8 +2557,9 @@ function actionForMissing(pathName, state) {
2540
2557
  reason: "managed block file is missing or stale",
2541
2558
  };
2542
2559
  }
2543
- function modelMergeTaskFor(pathName, state, allPaths) {
2560
+ function modelMergeTaskFor(pathName, state, allPaths, recordedState) {
2544
2561
  return {
2562
+ taskId: `init-merge-${sha256Text(pathName).slice(0, 12)}`,
2545
2563
  path: pathName,
2546
2564
  reason: "target file exists but does not match the current loop-agent initialization surface",
2547
2565
  sourcePath: state.sourcePath,
@@ -2562,8 +2580,14 @@ function modelMergeTaskFor(pathName, state, allPaths) {
2562
2580
  "loop-agent init check-update --repo-root . --json",
2563
2581
  "loop-agent init doctor --repo-root .",
2564
2582
  "loop-agent inspect --repo-root .",
2583
+ "loop-agent docs audit --repo-root .",
2565
2584
  "bash scripts/check-repo.sh",
2566
2585
  ],
2586
+ evidence: {
2587
+ baseSha256: recordedState?.files[pathName]?.currentSha256,
2588
+ currentSha256: state.currentSha256,
2589
+ desiredSha256: state.sourceSha256,
2590
+ },
2567
2591
  };
2568
2592
  }
2569
2593
  function recommendedNextFor(report) {
@@ -2588,6 +2612,7 @@ const CLIENT_RECOVERY_SURFACE_PATHS = new Set([
2588
2612
  OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
2589
2613
  OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH,
2590
2614
  PI_CONTEXT_OVERFLOW_EXTENSION_PATH,
2615
+ PI_PROJECT_SETTINGS_PATH,
2591
2616
  ]);
2592
2617
  function isClientRecoverySurfacePath(pathName) {
2593
2618
  return CLIENT_RECOVERY_SURFACE_PATHS.has(pathName);
@@ -2644,6 +2669,23 @@ export async function checkInitUpdate(input) {
2644
2669
  if (skipClientRecoverySurfaces && isClientRecoverySurfacePath(pathName)) {
2645
2670
  continue;
2646
2671
  }
2672
+ // `.pi/settings.json` is a structured project configuration: fill only
2673
+ // absent nested recovery fields and preserve every explicit project value.
2674
+ if (pathName === PI_PROJECT_SETTINGS_PATH &&
2675
+ state.relationship === "local-existing-unknown") {
2676
+ const projectPi = await inspectProjectPiSettings({ repoRoot });
2677
+ if (projectPi.action === "write") {
2678
+ deterministicActions.push({
2679
+ type: "merge-pi-project-settings",
2680
+ path: pathName,
2681
+ reason: "fill missing project Pi retry/compaction fields without overwriting explicit values",
2682
+ });
2683
+ continue;
2684
+ }
2685
+ if (projectPi.reason === "matches" || projectPi.reason === "enabled-false") {
2686
+ continue;
2687
+ }
2688
+ }
2647
2689
  if (state.relationship === "missing-from-target") {
2648
2690
  if (retiredTargetPaths.has(pathName))
2649
2691
  continue;
@@ -2661,6 +2703,18 @@ export async function checkInitUpdate(input) {
2661
2703
  continue;
2662
2704
  }
2663
2705
  if (state.relationship === "local-existing-unknown") {
2706
+ const acceptedMerge = recordedState?.files[pathName]?.acceptedMerge;
2707
+ if (acceptedMerge) {
2708
+ if (acceptedMerge.currentSha256 === state.currentSha256 &&
2709
+ acceptedMerge.desiredSha256 === state.sourceSha256) {
2710
+ continue;
2711
+ }
2712
+ // A previously accepted semantic merge is user-preserving ownership.
2713
+ // A later desired/current change must be merged again, never refreshed
2714
+ // as an unchanged package-owned file.
2715
+ modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
2716
+ continue;
2717
+ }
2664
2718
  if (state.mode === "directory") {
2665
2719
  humanDecisions.push({
2666
2720
  path: pathName,
@@ -2679,7 +2733,7 @@ export async function checkInitUpdate(input) {
2679
2733
  });
2680
2734
  }
2681
2735
  else {
2682
- modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths));
2736
+ modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
2683
2737
  }
2684
2738
  }
2685
2739
  }
@@ -2736,10 +2790,8 @@ export async function checkInitUpdate(input) {
2736
2790
  },
2737
2791
  };
2738
2792
  const recommendedNext = recommendedNextFor(partial);
2739
- // Pi user config is never part of project surface hash; only report semantics.
2740
- const piInspection = await inspectPiRetryConfig({
2741
- homeDir: input.homeDir,
2742
- });
2793
+ // Project settings are part of the surface. Do not inspect user home by default.
2794
+ const piInspection = await inspectProjectPiSettings({ repoRoot });
2743
2795
  return {
2744
2796
  ...partial,
2745
2797
  ok: deterministicActions.length === 0 &&
@@ -2752,6 +2804,9 @@ export async function checkInitUpdate(input) {
2752
2804
  };
2753
2805
  }
2754
2806
  async function applySafeAction(input) {
2807
+ if (input.action.type === "merge-pi-project-settings") {
2808
+ return (await applyProjectPiSettingsMerge({ repoRoot: input.repoRoot })).wrote === true;
2809
+ }
2755
2810
  if (input.action.type === "add-harness-schema-ref") {
2756
2811
  const target = path.join(input.repoRoot, "harness.json");
2757
2812
  if (!(await exists(target)))
@@ -3141,13 +3196,19 @@ function parseInitArgs(repoRoot, args) {
3141
3196
  let markdown = false;
3142
3197
  let bootstrapSurface = false;
3143
3198
  let applySafe = false;
3199
+ let runId;
3200
+ let upgradeStatus = false;
3201
+ let upgradeContinue = false;
3202
+ let upgradeReport = false;
3203
+ let versionChoice;
3144
3204
  for (let i = 0; i < args.length; i += 1) {
3145
3205
  const arg = args[i];
3146
3206
  if ((arg === "instructions" ||
3147
3207
  arg === "doctor" ||
3148
3208
  arg === "check-update" ||
3149
3209
  arg === "update" ||
3150
- arg === "reconcile") &&
3210
+ arg === "reconcile" ||
3211
+ arg === "upgrade") &&
3151
3212
  !subcommand) {
3152
3213
  subcommand = arg;
3153
3214
  continue;
@@ -3188,6 +3249,20 @@ function parseInitArgs(repoRoot, args) {
3188
3249
  bootstrapSurface = true;
3189
3250
  else if (arg === "--apply-safe")
3190
3251
  applySafe = true;
3252
+ else if (arg === "--run-id")
3253
+ runId = args[++i];
3254
+ else if (arg.startsWith("--run-id="))
3255
+ runId = arg.slice("--run-id=".length);
3256
+ else if (arg === "--status")
3257
+ upgradeStatus = true;
3258
+ else if (arg === "--continue")
3259
+ upgradeContinue = true;
3260
+ else if (arg === "--report")
3261
+ upgradeReport = true;
3262
+ else if (arg === "--version-choice")
3263
+ versionChoice = args[++i];
3264
+ else if (arg.startsWith("--version-choice="))
3265
+ versionChoice = arg.slice("--version-choice=".length);
3191
3266
  else if (arg.startsWith("-"))
3192
3267
  throw new Error(`unknown init flag: ${arg}`);
3193
3268
  else
@@ -3195,6 +3270,9 @@ function parseInitArgs(repoRoot, args) {
3195
3270
  }
3196
3271
  if (profile !== "full" && profile !== "minimal")
3197
3272
  throw new Error("init --profile must be full or minimal");
3273
+ if (versionChoice && !["current", "upgrade", "cancel", "retry"].includes(versionChoice)) {
3274
+ throw new Error("init upgrade --version-choice must be current|upgrade|cancel|retry");
3275
+ }
3198
3276
  return {
3199
3277
  repoRoot,
3200
3278
  projectName,
@@ -3209,6 +3287,11 @@ function parseInitArgs(repoRoot, args) {
3209
3287
  markdown,
3210
3288
  bootstrapSurface,
3211
3289
  applySafe,
3290
+ runId,
3291
+ upgradeStatus,
3292
+ upgradeContinue,
3293
+ upgradeReport,
3294
+ versionChoice,
3212
3295
  };
3213
3296
  }
3214
3297
  export async function runInit(repoRoot, rawArgs, dependencies) {
@@ -3244,6 +3327,24 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3244
3327
  : formatInitUpdateResult(result));
3245
3328
  return;
3246
3329
  }
3330
+ if (parsed.subcommand === "upgrade") {
3331
+ const { runInitUpgrade } = await import("./init-upgrade.js");
3332
+ const selected = [parsed.upgradeStatus, parsed.upgradeContinue, parsed.upgradeReport].filter(Boolean).length;
3333
+ if (selected > 1)
3334
+ throw new Error("init upgrade accepts only one of --status, --continue, or --report");
3335
+ const result = await runInitUpgrade({
3336
+ repoRoot: parsed.repoRoot,
3337
+ runId: parsed.runId,
3338
+ mode: parsed.upgradeStatus ? "status" : parsed.upgradeContinue ? "continue" : parsed.upgradeReport ? "report" : "start",
3339
+ versionChoice: parsed.versionChoice,
3340
+ readRuntimeActivity: dependencies.readRuntimeActivity,
3341
+ });
3342
+ if ("markdown" in result)
3343
+ console.log(result.markdown);
3344
+ else
3345
+ console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitUpgradeResult(result));
3346
+ return;
3347
+ }
3247
3348
  if (parsed.subcommand === "reconcile") {
3248
3349
  const result = await runInitReconcile({
3249
3350
  ...parsed,
@@ -3257,6 +3358,16 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3257
3358
  const result = await initializeLoopAgentProject(parsed);
3258
3359
  console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitResult(result));
3259
3360
  }
3361
+ function formatInitUpgradeResult(result) {
3362
+ return [
3363
+ `loop-agent init upgrade: ${result.status}`,
3364
+ `runId: ${result.runId}`,
3365
+ `phase: ${result.phase}`,
3366
+ `modelMergeTasks: ${result.modelMergeTasks.length}`,
3367
+ `humanDecisions: ${result.humanDecisions.length}`,
3368
+ `next: ${result.nextAction}`,
3369
+ ].join("\n");
3370
+ }
3260
3371
  function formatInitUpdateResult(result) {
3261
3372
  return [
3262
3373
  "loop-agent init update complete",
@@ -126,6 +126,69 @@ export const workflowPolicySchema = z
126
126
  })
127
127
  .optional()
128
128
  .default({});
129
+ /** Night Scheduler tracked policy (ADR 0009). Defaults are package-safe; company repos may require task cards. */
130
+ export const nightSchedulerMisfirePolicySchema = z.enum([
131
+ "run-immediately",
132
+ "wait-next-window",
133
+ "human-required",
134
+ ]);
135
+ export const nightSchedulerMergePolicySchema = z.enum([
136
+ "manual-on-success",
137
+ "auto-on-success",
138
+ ]);
139
+ export const nightSchedulerTraceabilitySchema = z
140
+ .object({
141
+ required: z.boolean().optional().default(false),
142
+ label: z.string().min(1).optional().default("Task Card"),
143
+ pattern: z
144
+ .string()
145
+ .min(1)
146
+ .optional()
147
+ .default("^[A-Z][A-Z0-9]+-[A-Z0-9]+#[0-9]+$"),
148
+ commitMessageTemplate: z
149
+ .string()
150
+ .min(1)
151
+ .optional()
152
+ .default("{summary}\n\n{workItemRef}"),
153
+ })
154
+ .superRefine((value, ctx) => {
155
+ if (!value.commitMessageTemplate.includes("{workItemRef}")) {
156
+ ctx.addIssue({
157
+ code: z.ZodIssueCode.custom,
158
+ message: "workerPolicy.nightScheduler.traceability.commitMessageTemplate must include {workItemRef}",
159
+ path: ["commitMessageTemplate"],
160
+ });
161
+ }
162
+ });
163
+ export const nightSchedulerAutoFollowUpSchema = z.object({
164
+ /** Default off (ADR 0010). When true, failed night executions may draft Follow-up only. */
165
+ enabled: z.boolean().optional().default(false),
166
+ /** MVP only supports draft-only (never auto-approve / never auto-Ready). */
167
+ mode: z.enum(["draft-only"]).optional().default("draft-only"),
168
+ });
169
+ export const nightSchedulerPolicySchema = z.object({
170
+ enabled: z.boolean().optional().default(false),
171
+ timezone: z.string().min(1).optional().default("Asia/Shanghai"),
172
+ allowedHours: z
173
+ .array(z.string().regex(/^\d{2}:\d{2}-\d{2}:\d{2}$/))
174
+ .optional()
175
+ .default(["00:00-08:00"]),
176
+ maxConcurrency: z.number().int().positive().optional().default(1),
177
+ maxAutoAttempts: z.number().int().positive().optional().default(1),
178
+ misfirePolicy: nightSchedulerMisfirePolicySchema
179
+ .optional()
180
+ .default("wait-next-window"),
181
+ keepFailedWorktree: z.boolean().optional().default(true),
182
+ mergePolicy: nightSchedulerMergePolicySchema
183
+ .optional()
184
+ .default("manual-on-success"),
185
+ traceability: nightSchedulerTraceabilitySchema.optional().default({}),
186
+ /** ADR 0010 — default off; omit or enabled:false keeps night silent on follow-ups. */
187
+ autoFollowUp: nightSchedulerAutoFollowUpSchema.optional(),
188
+ });
189
+ export const workerPolicySchema = z.object({
190
+ nightScheduler: nightSchedulerPolicySchema.optional().default({}),
191
+ });
129
192
  export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
130
193
  export const harnessManifestSchema = z
131
194
  .object({
@@ -169,6 +232,8 @@ export const harnessManifestSchema = z
169
232
  .optional()
170
233
  .default({}),
171
234
  workflowPolicy: workflowPolicySchema,
235
+ /** Optional; absent means Night Scheduler uses package defaults when enabled by callers. */
236
+ workerPolicy: workerPolicySchema.optional(),
172
237
  worktree: worktreeManifestConfigSchema.optional(),
173
238
  executors: z
174
239
  .record(z.string(), executorManifestSchema)