@tea-agent/loop-agent 0.28.2-beta.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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +11 -1
- package/dist/cli/command-definitions.js +2 -1
- package/dist/commands/client-recovery.js +111 -8
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/init-upgrade.js +2479 -0
- package/dist/commands/init.js +120 -9
- package/dist/governance/manifest-types.js +65 -0
- package/dist/shared/operator/capabilities.js +350 -2
- package/dist/task/worktree.js +256 -39
- package/dist/worker/cli.js +22 -12
- package/dist/worker/console/chat/workspace-landing.js +16 -6
- package/dist/worker/console/observe-health-match.js +2 -0
- package/dist/worker/console/observe-link.js +4 -0
- package/dist/worker/console/operator-actions.js +183 -4
- package/dist/worker/console/operator-selection.js +13 -0
- package/dist/worker/console/static/assets/index-BfRgtLF4.js +29 -0
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/observe/health.js +1 -0
- package/dist/worker/observe/night-jobs.js +104 -0
- package/dist/worker/observe/routes.js +48 -0
- package/dist/worker/observe/static/app.js +3 -0
- package/dist/worker/observe/static/constants.js +1 -0
- package/dist/worker/observe/static/index.html +47 -0
- package/dist/worker/observe/static/router.js +10 -0
- package/dist/worker/observe/static/shell-chrome.js +1 -0
- package/dist/worker/observe/static/views/night.js +201 -0
- package/dist/worker/report/morning-report.js +56 -16
- package/dist/worker/run-task/execute-prepared-task.js +153 -0
- package/dist/worker/runner/single-task-attempt.js +147 -0
- package/dist/worker/scheduler/admission.js +536 -0
- package/dist/worker/scheduler/auto-followup.js +99 -0
- package/dist/worker/scheduler/cli.js +539 -0
- package/dist/worker/scheduler/dispatcher.js +503 -0
- package/dist/worker/scheduler/doctor.js +346 -0
- package/dist/worker/scheduler/evidence.js +170 -0
- package/dist/worker/scheduler/git-base.js +52 -0
- package/dist/worker/scheduler/index.js +23 -0
- package/dist/worker/scheduler/lease.js +114 -0
- package/dist/worker/scheduler/lifecycle.js +348 -0
- package/dist/worker/scheduler/lock.js +80 -0
- package/dist/worker/scheduler/morning-window.js +161 -0
- package/dist/worker/scheduler/night-git-finalizer.js +88 -0
- package/dist/worker/scheduler/night-harvest.js +421 -0
- package/dist/worker/scheduler/paths.js +84 -0
- package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
- package/dist/worker/scheduler/recovery.js +277 -0
- package/dist/worker/scheduler/reservation.js +146 -0
- package/dist/worker/scheduler/retry.js +199 -0
- package/dist/worker/scheduler/scheduler-loop.js +272 -0
- package/dist/worker/scheduler/store.js +275 -0
- package/dist/worker/scheduler/traceability.js +54 -0
- package/dist/worker/scheduler/trigger.js +258 -0
- package/dist/worker/scheduler/types.js +369 -0
- package/dist/worker/scheduler/workspace-adapter.js +91 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +2 -102
- package/docs/architecture/runtime-boundaries.md +9 -0
- package/docs/init-surface.manifest.json +9 -2
- package/docs/templates/harness.schema.json +107 -0
- package/docs/templates/init-managed-agents.md +18 -8
- package/harness.json +22 -0
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +28 -36
- package/skills/loop-agent/references/command-reference.md +40 -16
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-CNO7n6qB.js +0 -29
package/dist/commands/init.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
//
|
|
2740
|
-
const piInspection = await
|
|
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)
|