@kylecheng3146/agent-ops 0.11.3 → 0.11.4
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 +8 -1
- package/dist/packages/cli/src/args.js +18 -3
- package/dist/packages/cli/src/cli.js +5 -0
- package/dist/packages/cli/src/commands/advance.js +2 -1
- package/dist/packages/cli/src/commands/init.js +2 -5
- package/dist/packages/cli/src/commands/run.js +9 -1
- package/dist/packages/cli/src/run-deps.js +18 -2
- package/dist/runtime/src/install/doctor.js +3 -5
- package/dist/runtime/src/install/harness.js +2 -1
- package/dist/runtime/src/install/plan.js +1 -1
- package/dist/runtime/src/parallel/finish.js +7 -4
- package/dist/runtime/src/parallel/integrate.js +6 -4
- package/dist/runtime/src/run/land.js +145 -0
- package/dist/runtime/src/run/service.js +34 -0
- package/docs/en/spec/harness-adapters.md +2 -1
- package/docs/en/spec/loop-engineering.md +13 -0
- package/docs/zh-TW/spec/harness-adapters.md +2 -2
- package/docs/zh-TW/spec/loop-engineering.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,8 +53,14 @@ agent-ops run logs <run-id>
|
|
|
53
53
|
agent-ops run respond <run-id> --question-id <id> --answer "Clarification"
|
|
54
54
|
agent-ops run stop <run-id>
|
|
55
55
|
agent-ops run resume <run-id>
|
|
56
|
+
agent-ops run land <run-id> [--task <task-id>]... --yes
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
`run land` merges the delivered children of a paused, blocked or budget-limited
|
|
60
|
+
run ahead of the rest: it verifies them, runs the two-round review and finishes
|
|
61
|
+
with a receipt in a separate landing worktree, while the run stays resumable and
|
|
62
|
+
its final proof re-verifies the whole goal.
|
|
63
|
+
|
|
58
64
|
`--host claude` uses Claude Code; `--wait` waits for a terminal or input state.
|
|
59
65
|
The coordinator counts toward the two writer slots. Native processes write only
|
|
60
66
|
under current leases in separate worktrees. Active time counts once across parallel
|
|
@@ -268,7 +274,8 @@ question through `agent-ops run respond`. A start refused with
|
|
|
268
274
|
`WORKTREE_NESTED` falls back to the subtask flow; `RUN_REPO_UNTRUSTED` stops and
|
|
269
275
|
asks you. A run that ends without completing is reported with its status, last
|
|
270
276
|
log events, and the `agent-ops run resume <id>` / `agent-ops run stop <id>`
|
|
271
|
-
commands
|
|
277
|
+
commands (plus `agent-ops run land <id> --yes` when it has delivered tasks); it
|
|
278
|
+
is never resumed or landed automatically. agy keeps the subtask flow.
|
|
272
279
|
Init and update pre-authorize `agent-ops run` for Claude Code and Codex.
|
|
273
280
|
|
|
274
281
|
```bash
|
|
@@ -922,7 +922,7 @@ export function parseArgs(argv) {
|
|
|
922
922
|
};
|
|
923
923
|
}
|
|
924
924
|
function parseRunArgs(argv) {
|
|
925
|
-
const actions = ["start", "status", "logs", "resume", "stop", "respond"];
|
|
925
|
+
const actions = ["start", "status", "logs", "resume", "stop", "respond", "land"];
|
|
926
926
|
if (argv.includes("--help") || argv.includes("-h"))
|
|
927
927
|
return { command: "help", helpTopic: "run", profiles: [], dryRun: false, json: false, yes: false, rerun: false };
|
|
928
928
|
const action = actions.includes(argv[0]) ? argv[0] : "start";
|
|
@@ -937,21 +937,30 @@ function parseRunArgs(argv) {
|
|
|
937
937
|
positional = token;
|
|
938
938
|
continue;
|
|
939
939
|
}
|
|
940
|
-
if (seen.has(token) && token !== "--retry-approval")
|
|
940
|
+
if (seen.has(token) && token !== "--retry-approval" && token !== "--task")
|
|
941
941
|
duplicate(token);
|
|
942
942
|
seen.add(token);
|
|
943
943
|
if (token === "--json") {
|
|
944
944
|
args.json = true;
|
|
945
945
|
continue;
|
|
946
946
|
}
|
|
947
|
+
if (token === "--yes") {
|
|
948
|
+
args.yes = true;
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
947
951
|
if (token === "--wait") {
|
|
948
952
|
args.wait = true;
|
|
949
953
|
continue;
|
|
950
954
|
}
|
|
951
|
-
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval", "--retry-delivery"].includes(token))
|
|
955
|
+
if (!["--host", "--goal-file", "--time-budget", "--jobs", "--question-id", "--answer", "--answer-file", "--retry-approval", "--retry-delivery", "--task"].includes(token))
|
|
952
956
|
throw new CliArgumentError("CLI_UNKNOWN_OPTION", `Unknown run option: ${token}`, token);
|
|
953
957
|
const value = readOptionValue(argv, i++, token);
|
|
954
958
|
switch (token) {
|
|
959
|
+
case "--task":
|
|
960
|
+
if (!/^[A-Za-z0-9._:/-]{1,256}$/u.test(value) || args.landTaskIds?.includes(value))
|
|
961
|
+
invalidValue(token, value);
|
|
962
|
+
args.landTaskIds = [...(args.landTaskIds ?? []), value];
|
|
963
|
+
break;
|
|
955
964
|
case "--retry-delivery":
|
|
956
965
|
if (!/^task-[a-z0-9-]{1,128}$/u.test(value))
|
|
957
966
|
invalidValue(token, value);
|
|
@@ -991,6 +1000,12 @@ function parseRunArgs(argv) {
|
|
|
991
1000
|
}
|
|
992
1001
|
if (args.retryApprovalIds !== undefined && action !== "resume")
|
|
993
1002
|
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-approval requires run resume.");
|
|
1003
|
+
if (args.landTaskIds !== undefined && action !== "land")
|
|
1004
|
+
throw new CliArgumentError("CLI_INVALID_OPTION", "--task requires run land.");
|
|
1005
|
+
if (args.yes !== (action === "land"))
|
|
1006
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", action === "land"
|
|
1007
|
+
? "Run land requires --yes to authorize its verification and both reviewer sessions."
|
|
1008
|
+
: "--yes is accepted only by run land.");
|
|
994
1009
|
if (args.retryDeliveryTaskId !== undefined && (action !== "resume" || args.retryApprovalIds !== undefined))
|
|
995
1010
|
throw new CliArgumentError("CLI_INVALID_OPTION", "--retry-delivery requires run resume without --retry-approval.");
|
|
996
1011
|
if (action === "start") {
|
|
@@ -130,6 +130,7 @@ The URL carries an access token; do not share it.
|
|
|
130
130
|
agent-ops run resume <run-id> [--retry-approval <auth-event-id>]... [--json]
|
|
131
131
|
agent-ops run resume <run-id> --retry-delivery <task-id> [--json]
|
|
132
132
|
agent-ops run respond <run-id> --question-id <id> <--answer <text>|--answer-file <path>> [--json]
|
|
133
|
+
agent-ops run land <run-id> [--task <task-id>]... --yes [--json]
|
|
133
134
|
|
|
134
135
|
New runs have no total time cap. Manual resume upgrades legacy finite runs and keeps accounting.
|
|
135
136
|
Command/review timeouts, provider limits and stop controls remain effective.
|
|
@@ -144,6 +145,10 @@ Status includes phase, original cause, recorded sealed proof, cleanup state,
|
|
|
144
145
|
resumability and remediation. Resume revalidates authority and sealed artifacts;
|
|
145
146
|
receipt-bound recovery executes only missing cleanup steps. Resolve the saved
|
|
146
147
|
blocker before retrying: unchanged cleanup inputs are refused.
|
|
148
|
+
land merges delivered children of a paused, blocked or budget-limited run ahead
|
|
149
|
+
of the rest: it verifies them, runs the two-round review and finishes with a
|
|
150
|
+
receipt, leaving the run resumable. --task picks a subset; each one's
|
|
151
|
+
dependencies must be picked too or already landed.
|
|
147
152
|
`,
|
|
148
153
|
init: `Usage: agent-ops init [options]
|
|
149
154
|
|
|
@@ -102,7 +102,7 @@ export async function runAdvanceCommand(options) {
|
|
|
102
102
|
if (parent.status !== "active" || parent.task.parentTaskId !== undefined) {
|
|
103
103
|
throw new AgentOpsError("ADVANCE_PARENT_INVALID", "Advance requires an active root task in the coordinator worktree.");
|
|
104
104
|
}
|
|
105
|
-
const children = await integrateSessionChildren(deps, record, parent.task.id);
|
|
105
|
+
const children = await integrateSessionChildren(deps, record, parent.task.id, undefined, options.children);
|
|
106
106
|
const step = options.step ?? runAdvanceStep;
|
|
107
107
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
108
108
|
const target = await git(deps, mainRoot, ["rev-parse", `${record.targetBranch}^{commit}`]);
|
|
@@ -159,6 +159,7 @@ export async function runAdvanceCommand(options) {
|
|
|
159
159
|
...(noChangePaths === undefined ? {} : { noChangePaths }) });
|
|
160
160
|
const sourceFingerprint = await calculateSourceFingerprint(record.path, scope, runner(deps, record.path));
|
|
161
161
|
const finalProof = { target, head, sourceFingerprint, children,
|
|
162
|
+
...(options.children === undefined ? {} : { keepChildren: true }),
|
|
162
163
|
...(noChangePaths === undefined ? {} : { noChange: { sourceCommit: head,
|
|
163
164
|
contractDigest: await deps.tasks(record.path).treeContract(parent.task.id), reviewScope: scope,
|
|
164
165
|
artifactRefs: Object.values((await deps.tasks(record.path).status({ taskId: parent.task.id })).evidence).flat() } }) };
|
|
@@ -127,13 +127,10 @@ export async function runInitCommand(options) {
|
|
|
127
127
|
: { worktree: worktreeConfig })
|
|
128
128
|
});
|
|
129
129
|
const trust = await trustChange(options, plan);
|
|
130
|
-
//
|
|
131
|
-
// completion needs current PASS evidence from a required verifier, so the
|
|
132
|
-
// loop can never close. Detection stays conservative on purpose — it will
|
|
133
|
-
// not guess a test command — which makes saying so out loud the whole fix.
|
|
130
|
+
// Empty commands block legacy criteria, not typed acceptance backed by runners.
|
|
134
131
|
const verificationWarnings = plan.config.verification.commands.length === 0
|
|
135
132
|
? [
|
|
136
|
-
"No verification command is configured
|
|
133
|
+
"No verification command is configured; legacy criteria need a required verifier. " +
|
|
137
134
|
"Add verification.commands to .agent-ops/config.json." +
|
|
138
135
|
(plan.verificationBlockers.length === 0
|
|
139
136
|
? ""
|
|
@@ -9,7 +9,7 @@ function actionOf(args) {
|
|
|
9
9
|
const value = dynamic(args).runAction ?? dynamic(args).action;
|
|
10
10
|
if (value === undefined || value === "start")
|
|
11
11
|
return "start";
|
|
12
|
-
if (["status", "logs", "resume", "stop", "respond"].includes(String(value)))
|
|
12
|
+
if (["status", "logs", "resume", "stop", "respond", "land"].includes(String(value)))
|
|
13
13
|
return value;
|
|
14
14
|
throw new AgentOpsError("RUN_ACTION_INVALID", `Unsupported run action: ${String(value)}`);
|
|
15
15
|
}
|
|
@@ -109,6 +109,14 @@ export async function runRunCommand(options) {
|
|
|
109
109
|
}
|
|
110
110
|
if (action === "resume")
|
|
111
111
|
return okEnvelope("RUN_RESUMED", data(action, await options.service.resume(runId, options.args.retryApprovalIds, true, options.args.retryDeliveryTaskId)));
|
|
112
|
+
if (action === "land") {
|
|
113
|
+
if (options.service.land === undefined)
|
|
114
|
+
throw new AgentOpsError("RUN_LAND_UNSUPPORTED", "This CLI cannot land run deliveries.");
|
|
115
|
+
const landed = await options.service.land(runId, options.args.landTaskIds);
|
|
116
|
+
const message = `Landed ${landed.taskIds.join(", ")} from run ${runId}. Receipt: ${landed.receipt}.\n` +
|
|
117
|
+
`The run stays ${landed.state.status}; continue the rest with: agent-ops run resume ${runId}`;
|
|
118
|
+
return okEnvelope("RUN_LANDED", { ...data(action, { state: landed.state, message }), landedTaskIds: landed.taskIds, receipt: landed.receipt });
|
|
119
|
+
}
|
|
112
120
|
if (action === "stop")
|
|
113
121
|
return okEnvelope("RUN_STOP_REQUESTED", data(action, await options.service.stop(runId, options.args.answer ?? "user requested stop")));
|
|
114
122
|
const questionId = options.args.questionId;
|
|
@@ -9,7 +9,8 @@ import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
|
9
9
|
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
10
10
|
import { resolveCheckouts, readWorktreeRecord, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
|
|
11
11
|
import { createLaunchdDescriptor, LaunchdController, readBootIdentity, readGuiLoginIdentity } from "../../../runtime/src/run/macOS.js";
|
|
12
|
-
import { FileRunRepository, RunService, validateDeliveryRetry } from "../../../runtime/src/run/service.js";
|
|
12
|
+
import { FileRunRepository, RunService, landingMarkerLive, validateDeliveryRetry } from "../../../runtime/src/run/service.js";
|
|
13
|
+
import { landRun } from "../../../runtime/src/run/land.js";
|
|
13
14
|
import { readPrivateFile, writePrivateFile } from "../../../runtime/src/security/permissions.js";
|
|
14
15
|
import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
|
|
15
16
|
import { worktreeDependencies, gitRunner, trustStore } from "./parallel-deps.js";
|
|
@@ -28,6 +29,7 @@ import { nativeProcessIdentity } from "../../../runtime/src/run/transport.js";
|
|
|
28
29
|
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
29
30
|
import { RunControlService } from "../../../runtime/src/run/controls.js";
|
|
30
31
|
import { recordRunPhase } from "../../../runtime/src/run/phase.js";
|
|
32
|
+
import { runAdvanceCommand } from "./commands/advance.js";
|
|
31
33
|
/** Worktree construction happens before a native lease exists; it must never write global trust. */
|
|
32
34
|
export async function runWorktreeDependencies(deps, state) {
|
|
33
35
|
state = await ensureRunPolicy(new FileRunRepository(join(state.commonDir, "agent-ops/runs"), state.commonDir), state);
|
|
@@ -213,6 +215,8 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
213
215
|
};
|
|
214
216
|
const resume = async (runId, retryApprovalIds = [], upgradeBudget = true, retryDeliveryTaskId) => {
|
|
215
217
|
const before = await service.status(runId);
|
|
218
|
+
if (landingMarkerLive(before.landing))
|
|
219
|
+
throw new AgentOpsError("RUN_LANDING_IN_PROGRESS", `Run ${runId} is landing delivered work; wait for run land to finish.`);
|
|
216
220
|
const retry = retryDeliveryTaskId === undefined ? null : validateDeliveryRetry(before, retryDeliveryTaskId);
|
|
217
221
|
const runtimeHash = await runRuntimeHash();
|
|
218
222
|
const policy = await readRunPolicy(before, before.policyBinding?.runtimeHash ?? runtimeHash, Date.now(), true);
|
|
@@ -315,6 +319,18 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
315
319
|
}
|
|
316
320
|
return { state, message: `Run ${runId} stopped; registered native process death was confirmed.` };
|
|
317
321
|
};
|
|
322
|
+
const land = async (runId, taskIds) => {
|
|
323
|
+
if (checkouts.currentRoot !== checkouts.mainRoot)
|
|
324
|
+
throw new AgentOpsError("WORKTREE_NESTED", "Land a run from the main checkout.");
|
|
325
|
+
return await landRun({ repository, runId, ...(taskIds === undefined ? {} : { taskIds }), deps,
|
|
326
|
+
advance: async (input) => {
|
|
327
|
+
const result = await runAdvanceCommand({ ...input, deps });
|
|
328
|
+
const receipt = result.data?.receipt;
|
|
329
|
+
if (result.status !== "ok" || typeof receipt !== "string")
|
|
330
|
+
throw new AgentOpsError(result.code, "Landing proof did not finish with a receipt.");
|
|
331
|
+
return { receipt };
|
|
332
|
+
} });
|
|
333
|
+
};
|
|
318
334
|
const respond = async (runId, questionId, answer) => {
|
|
319
335
|
const state = await service.status(runId);
|
|
320
336
|
if (!state.questions.some(q => q.questionId === questionId && q.answeredAt === null))
|
|
@@ -331,7 +347,7 @@ export async function productionRunContext(cwd, options = {}) {
|
|
|
331
347
|
}
|
|
332
348
|
};
|
|
333
349
|
return { ...checkouts, targetBranch: target.stdout.trim(), ownerSessionId: randomUUID(), repository,
|
|
334
|
-
service: { start, resume, stop, respond, status: service.status.bind(service), logs: service.logs.bind(service) },
|
|
350
|
+
service: { start, resume, stop, respond, land, status: service.status.bind(service), logs: service.logs.bind(service) },
|
|
335
351
|
waitForCompletion: async (runId) => {
|
|
336
352
|
for (;;) {
|
|
337
353
|
const state = await service.status(runId);
|
|
@@ -380,17 +380,15 @@ async function checkRegistrationDrift(root, manifest, config) {
|
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
* configuration — it is a loop that cannot close, and an installation reaches
|
|
386
|
-
* that state quietly whenever stack detection declines to guess.
|
|
383
|
+
* Legacy criteria need required verifiers; typed acceptance can use runners
|
|
384
|
+
* even when no ordinary verification command is configured.
|
|
387
385
|
*/
|
|
388
386
|
function checkVerificationCommands(config) {
|
|
389
387
|
if (config === undefined) {
|
|
390
388
|
return check("verification-commands", "UNKNOWN", "Configuration could not be read, so verifiers are unknown.");
|
|
391
389
|
}
|
|
392
390
|
if (config.verification.commands.length === 0) {
|
|
393
|
-
return check("verification-commands", "DEGRADED", "No verification command is configured
|
|
391
|
+
return check("verification-commands", "DEGRADED", "No verification command is configured; legacy criteria need a required verifier.",
|
|
394
392
|
// Codeless on purpose: the fix is an edit to the configuration file, not
|
|
395
393
|
// an agent-ops command, and a code here would force a non-zero exit on
|
|
396
394
|
// every installation whose stack detection declined to guess.
|
|
@@ -347,7 +347,8 @@ function autoRunInstruction(descriptor, context) {
|
|
|
347
347
|
"report the `agent-ops run status",
|
|
348
348
|
"<id>` state and code, the last `agent-ops run logs <id>` events, and the",
|
|
349
349
|
"exact `agent-ops run resume <id>` and `agent-ops run stop <id>` commands,",
|
|
350
|
-
"
|
|
350
|
+
"plus `agent-ops run land <id> --yes` when it has delivered tasks,",
|
|
351
|
+
"then stop: never resume or land automatically or take over the run's work.",
|
|
351
352
|
""
|
|
352
353
|
];
|
|
353
354
|
}
|
|
@@ -88,7 +88,7 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
88
88
|
const verification = existing?.verification !== undefined &&
|
|
89
89
|
existing.verification.commands.length > 0
|
|
90
90
|
? existing.verification
|
|
91
|
-
: { commands: [...detectedCommands] };
|
|
91
|
+
: { ...existing?.verification, commands: [...detectedCommands] };
|
|
92
92
|
// Pre-0.11 DEBUG budget settings are dropped on rewrite.
|
|
93
93
|
const existingFeatures = existing === undefined ? undefined : (({ debug: _legacyDebug, ...rest }) => rest)(existing.features);
|
|
94
94
|
const effectiveWorktree = worktree !== undefined
|
|
@@ -318,9 +318,12 @@ export async function finishWorktree(deps, options) {
|
|
|
318
318
|
return await withActivity(commonDir, { kind: "finish", root: record.path, sessionId: record.sessionId, worktree: name }, async () => await withFinishLock(deps, commonDir, async () => {
|
|
319
319
|
const listing = await git(deps, mainRoot, ["worktree", "list", "--porcelain"], "WORKTREE_LIST_FAILED", "Cannot list cleanup worktrees.");
|
|
320
320
|
const records = await Promise.all(parseWorktreeListPorcelain(listing).map(child => readWorktreeRecord(child.path)));
|
|
321
|
+
const keepChildren = options.finalProof?.keepChildren === true;
|
|
321
322
|
const sessionChildren = records
|
|
322
|
-
.filter((child) => child !== null && child.path !== record.path &&
|
|
323
|
-
|
|
323
|
+
.filter((child) => child !== null && child.path !== record.path && (keepChildren
|
|
324
|
+
? options.finalProof.children.some(({ name }) => name === child.name)
|
|
325
|
+
: child.sessionId === record.sessionId &&
|
|
326
|
+
(child.agentId !== undefined || (record.runId !== undefined && child.runId === record.runId))));
|
|
324
327
|
if (options.finalProof?.recovery === true && record.runId !== undefined) {
|
|
325
328
|
const saved = await new FileRunRepository(join(commonDir, "agent-ops/runs"), commonDir).read(record.runId);
|
|
326
329
|
if (["receipt-written", "cleaned"].includes(saved?.integration?.status ?? ""))
|
|
@@ -579,7 +582,7 @@ export async function finishWorktree(deps, options) {
|
|
|
579
582
|
// this session began would leave it reading the merge as its own change.
|
|
580
583
|
// Only this worktree leaves the session; its other roots stay covered.
|
|
581
584
|
await mainGate.afterFinish(record.sessionId, record.path, after);
|
|
582
|
-
if (options.finalProof !== undefined) {
|
|
585
|
+
if (options.finalProof !== undefined && !keepChildren) {
|
|
583
586
|
for (const child of sessionChildren) {
|
|
584
587
|
await mainGate.afterFinish(record.sessionId, child.path, after);
|
|
585
588
|
}
|
|
@@ -593,7 +596,7 @@ export async function finishWorktree(deps, options) {
|
|
|
593
596
|
await attempt("trust", async () => await deps.trust.revoke(record.path, worktreeConfig));
|
|
594
597
|
if (receiptOptions !== undefined)
|
|
595
598
|
await markRunIntegration(commonDir, record, "receipt-written", "trust");
|
|
596
|
-
if (options.finalProof !== undefined) {
|
|
599
|
+
if (options.finalProof !== undefined && !keepChildren) {
|
|
597
600
|
for (const child of sessionChildren) {
|
|
598
601
|
await attempt(`child ${child.name}`, async () => {
|
|
599
602
|
const pinnedHead = await assertPinnedChild(child);
|
|
@@ -156,11 +156,13 @@ async function previous(record) {
|
|
|
156
156
|
return value;
|
|
157
157
|
}
|
|
158
158
|
/** Integrate only this session's agent-owned branches; local proof is never promoted as final proof. */
|
|
159
|
-
export async function integrateSessionChildren(deps, coordinator, parentTaskId, eligibleRunWorkerIds
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
export async function integrateSessionChildren(deps, coordinator, parentTaskId, eligibleRunWorkerIds,
|
|
160
|
+
/** Another session's children chosen by the caller (`run land`); replaces the session filter only. */
|
|
161
|
+
explicitChildren) {
|
|
162
|
+
const statuses = explicitChildren === undefined ? await listWorktrees(deps, coordinator.mainRoot) : [];
|
|
163
|
+
const children = (explicitChildren ?? statuses.map(({ record }) => record)
|
|
162
164
|
.filter((record) => record.name !== coordinator.name && record.sessionId === coordinator.sessionId &&
|
|
163
|
-
(record.agentId !== undefined || (coordinator.runId !== undefined && record.runId === coordinator.runId)))
|
|
165
|
+
(record.agentId !== undefined || (coordinator.runId !== undefined && record.runId === coordinator.runId))))
|
|
164
166
|
.filter(record => eligibleRunWorkerIds === undefined || (record.workerId !== undefined && eligibleRunWorkerIds.includes(record.workerId)))
|
|
165
167
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
166
168
|
const integrated = await previous(coordinator);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { calculateConfigHash } from "../config/hash.js";
|
|
5
|
+
import { sha256 } from "../fs/hash.js";
|
|
6
|
+
import { AgentOpsError } from "../fs/paths.js";
|
|
7
|
+
import { listWorktrees } from "../parallel/manage.js";
|
|
8
|
+
import { addWorktree, readWorktreeRecord, removeCheckout, sessionWorktreeName } from "../parallel/service.js";
|
|
9
|
+
import { readPrivateFile, writePrivateFile } from "../security/permissions.js";
|
|
10
|
+
import { landingMarkerLive, MAX_EVENTS } from "./service.js";
|
|
11
|
+
const LIVE_WORKER = ["assigned", "starting", "running", "idle", "handing-off"];
|
|
12
|
+
const LANDABLE = ["paused", "blocked", "budget-limited"];
|
|
13
|
+
/** Inside the landing worktree; names the run that owns it so another run never reuses it. */
|
|
14
|
+
const OWNER_FILE = join(".agent-ops", "tasks", "run-landing.json");
|
|
15
|
+
export function assertLandable(state) {
|
|
16
|
+
if (!LANDABLE.includes(state.status) || state.rootTaskId === null ||
|
|
17
|
+
state.workers.some(worker => LIVE_WORKER.includes(worker.status)))
|
|
18
|
+
throw new AgentOpsError("RUN_LAND_STATE_INVALID", `Run ${state.runId} is ${state.status}; land needs a paused, blocked or budget-limited run with a root task and no live worker.`);
|
|
19
|
+
}
|
|
20
|
+
/** Unlanded delivered children, or the requested ones; every dependency must be selected or already landed. */
|
|
21
|
+
export function selectLanding(state, requested) {
|
|
22
|
+
const landed = new Set((state.landings ?? []).flatMap(landing => landing.taskIds));
|
|
23
|
+
const eligible = state.tasks.filter(task => task.taskId !== state.rootTaskId && task.status === "delivered" &&
|
|
24
|
+
task.sourceCommit !== null && !landed.has(task.taskId));
|
|
25
|
+
for (const id of requested ?? [])
|
|
26
|
+
if (!eligible.some(task => task.taskId === id))
|
|
27
|
+
throw new AgentOpsError("RUN_LAND_TASK_INVALID", `${id} is not an unlanded delivered task of run ${state.runId}.`);
|
|
28
|
+
const selected = requested === undefined ? eligible : eligible.filter(task => requested.includes(task.taskId));
|
|
29
|
+
if (selected.length === 0)
|
|
30
|
+
throw new AgentOpsError("RUN_LAND_NOTHING", `Run ${state.runId} has no unlanded delivered task.`);
|
|
31
|
+
const ids = new Set(selected.map(task => task.taskId));
|
|
32
|
+
for (const task of selected)
|
|
33
|
+
for (const dependency of task.dependencies)
|
|
34
|
+
if (!ids.has(dependency) && !landed.has(dependency))
|
|
35
|
+
throw new AgentOpsError("RUN_LAND_DEPENDENCY_OPEN", `${task.taskId} depends on ${dependency}, which is neither selected nor landed.`);
|
|
36
|
+
return selected;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One landing session per attempt: a non-hex first letter keeps it apart from
|
|
40
|
+
* UUID sessions, and a fresh path per attempt never reuses a removed checkout.
|
|
41
|
+
*/
|
|
42
|
+
export function landingSessionId(runId, nonce = randomUUID()) {
|
|
43
|
+
return `l${sha256(`${runId}:${nonce}`).slice(0, 7)}${LANDING_SUFFIX}${runId}`;
|
|
44
|
+
}
|
|
45
|
+
const LANDING_SUFFIX = "-land-";
|
|
46
|
+
async function git(deps, cwd, args) {
|
|
47
|
+
const result = await deps.git(cwd, args);
|
|
48
|
+
if (result.exitCode !== 0)
|
|
49
|
+
throw new AgentOpsError("RUN_LAND_GIT_FAILED", `${args.join(" ")}: ${result.stderr.trim()}`);
|
|
50
|
+
return result.stdout.trim();
|
|
51
|
+
}
|
|
52
|
+
/** The worker checkout whose committed head is the frozen delivery. */
|
|
53
|
+
async function deliveredCheckout(deps, state, task) {
|
|
54
|
+
for (const worker of state.workers.filter(worker => worker.taskId === task.taskId && worker.worktree !== null)) {
|
|
55
|
+
const record = await readWorktreeRecord(worker.worktree);
|
|
56
|
+
if (record !== null && await git(deps, record.path, ["rev-parse", "HEAD"]) === task.sourceCommit)
|
|
57
|
+
return record;
|
|
58
|
+
}
|
|
59
|
+
throw new AgentOpsError("RUN_LAND_DELIVERY_MISSING", `No preserved checkout of ${task.taskId} is at its delivered commit ${task.sourceCommit}.`);
|
|
60
|
+
}
|
|
61
|
+
async function ownerOf(path) {
|
|
62
|
+
try {
|
|
63
|
+
const owner = JSON.parse(await readPrivateFile(join(path, OWNER_FILE), path) ?? "null");
|
|
64
|
+
return typeof owner?.runId === "string" ? owner.runId : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Worktrees kept by this run's failed attempts are removed first; one that claims the run without proof is refused. */
|
|
71
|
+
async function freshLandingWorktree(deps, state, sessionId) {
|
|
72
|
+
const claimed = (await listWorktrees(deps, state.root)).map(({ record }) => record)
|
|
73
|
+
.filter(record => record.sessionId.endsWith(LANDING_SUFFIX + state.runId));
|
|
74
|
+
for (const record of claimed)
|
|
75
|
+
if (await ownerOf(record.path) !== state.runId)
|
|
76
|
+
throw new AgentOpsError("RUN_LAND_WORKTREE_FOREIGN", `${record.path} claims run ${state.runId} without its owner record; it was left untouched.`);
|
|
77
|
+
for (const record of claimed)
|
|
78
|
+
await removeCheckout(deps, record, true);
|
|
79
|
+
const record = (await addWorktree(deps, { cwd: state.root, name: sessionWorktreeName(sessionId), sessionId,
|
|
80
|
+
from: state.targetBranch, targetBranch: state.targetBranch })).record;
|
|
81
|
+
await writePrivateFile(join(record.path, OWNER_FILE), JSON.stringify({ runId: state.runId }), record.path);
|
|
82
|
+
return record;
|
|
83
|
+
}
|
|
84
|
+
async function createLandingRoot(deps, state, record, selected) {
|
|
85
|
+
const config = await deps.loadConfig(record.path);
|
|
86
|
+
const required = config.verification.commands.filter(command => command.required).map(command => command.id);
|
|
87
|
+
if (required.length === 0)
|
|
88
|
+
throw new AgentOpsError("RUN_LAND_VERIFIER_REQUIRED", "Landing proof needs at least one required verification command.");
|
|
89
|
+
const coordinator = state.workers.find(worker => worker.workerId === state.coordinatorId)?.worktree ?? null;
|
|
90
|
+
const root = coordinator === null ? null : await deps.tasks(coordinator).status({ taskId: state.rootTaskId }).catch(() => null);
|
|
91
|
+
const ids = selected.map(task => task.taskId);
|
|
92
|
+
const base = await git(deps, record.path, ["rev-parse", "HEAD"]);
|
|
93
|
+
const criteria = [
|
|
94
|
+
{ id: "landed-verifiers", description: "Every required project verifier passes on the integrated candidate of the landed tasks.", verifierIds: required },
|
|
95
|
+
{ id: "landed-coherent", verifierIds: [],
|
|
96
|
+
description: "The landed subset is coherent on its own: it exposes no half-finished behavior and references no work of an undelivered run task.",
|
|
97
|
+
acceptance: { mode: "review-only", baselineCommit: base, bindings: [],
|
|
98
|
+
reviewOnlyReason: "partial-landing coherence: whether a subset stands alone without the undelivered tasks is a reviewer judgment, not a mechanical check" } }
|
|
99
|
+
];
|
|
100
|
+
// Typed acceptance needs repository context; the target head is the landing baseline.
|
|
101
|
+
const created = await deps.tasks(record.path, base).create({
|
|
102
|
+
title: `Land ${ids.join(", ")} from ${state.runId}`,
|
|
103
|
+
goal: `Land delivered tasks ${ids.join(", ")} of run ${state.runId} ahead of the rest of its goal:\n${state.goal}`.slice(0, 64_000),
|
|
104
|
+
intent: "Merge only the frozen deliveries of the listed run tasks; the run, its root task and its checkouts stay as they are.",
|
|
105
|
+
criteria, createdSourceCommit: base, policyConfigHash: calculateConfigHash(config), sessionId: record.sessionId,
|
|
106
|
+
...(root?.task.surface === undefined ? {} : { surface: root.task.surface })
|
|
107
|
+
});
|
|
108
|
+
return created.task.id;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `agent-ops run land`: prove and merge delivered children of a stopped run
|
|
112
|
+
* while the run stays resumable. Landed tasks remain delivered, so a later
|
|
113
|
+
* resume and finalize re-verify the whole goal on the new target.
|
|
114
|
+
*/
|
|
115
|
+
export async function landRun(options) {
|
|
116
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
117
|
+
let selected = [];
|
|
118
|
+
const held = await options.repository.mutate(options.runId, current => {
|
|
119
|
+
if (landingMarkerLive(current.landing))
|
|
120
|
+
throw new AgentOpsError("RUN_LANDING_IN_PROGRESS", `Run ${current.runId} is already landing delivered work.`);
|
|
121
|
+
assertLandable(current);
|
|
122
|
+
selected = selectLanding(current, options.taskIds);
|
|
123
|
+
return { ...current, landing: { processId: process.pid, hostname: hostname(), startedAt: now() } };
|
|
124
|
+
});
|
|
125
|
+
const taskIds = selected.map(task => task.taskId);
|
|
126
|
+
try {
|
|
127
|
+
const children = [];
|
|
128
|
+
for (const task of selected)
|
|
129
|
+
children.push(await deliveredCheckout(options.deps, held, task));
|
|
130
|
+
const sessionId = landingSessionId(held.runId);
|
|
131
|
+
const record = await freshLandingWorktree(options.deps, held, sessionId);
|
|
132
|
+
const parentTaskId = await createLandingRoot(options.deps, held, record, selected);
|
|
133
|
+
const { receipt } = await options.advance({ cwd: held.root, sessionId, parentTaskId, children });
|
|
134
|
+
const target = await git(options.deps, held.root, ["rev-parse", `${held.targetBranch}^{commit}`]);
|
|
135
|
+
const state = await options.repository.mutate(options.runId, current => ({ ...current, landing: null,
|
|
136
|
+
landings: [...(current.landings ?? []), { taskIds, receipt, target, landedAt: now() }],
|
|
137
|
+
events: [...current.events, { id: randomUUID(), at: now(), type: "integration", code: "RUN_PARTIAL_LANDED",
|
|
138
|
+
workerId: null, taskId: null, detail: `Landed ${taskIds.join(", ")} at ${target}; receipt ${receipt}.` }].slice(-MAX_EVENTS) }));
|
|
139
|
+
return { state, taskIds, receipt };
|
|
140
|
+
}
|
|
141
|
+
catch (cause) {
|
|
142
|
+
await options.repository.mutate(options.runId, current => ({ ...current, landing: null })).catch(() => undefined);
|
|
143
|
+
throw cause;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readdir } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
3
4
|
import { basename, join } from "node:path";
|
|
4
5
|
import { sha256 } from "../fs/hash.js";
|
|
5
6
|
import { AgentOpsError } from "../fs/paths.js";
|
|
@@ -10,6 +11,28 @@ import { redactSecrets } from "../security/redact.js";
|
|
|
10
11
|
export const RUN_PHASES = ["planning", "implementing", "verifying", "reviewing", "integrating"];
|
|
11
12
|
/** Writer slots are deliberately bounded; the coordinator may occupy one. */
|
|
12
13
|
export const MAX_ACTIVE_WORKERS = 2;
|
|
14
|
+
/**
|
|
15
|
+
* A marker from another host is treated as live: its process cannot be probed.
|
|
16
|
+
*
|
|
17
|
+
* ponytail: a reused pid on this host reads as live; clear the marker by hand if that ever bites.
|
|
18
|
+
*/
|
|
19
|
+
export function landingMarkerLive(marker) {
|
|
20
|
+
if (marker == null)
|
|
21
|
+
return false;
|
|
22
|
+
if (marker.hostname !== hostname())
|
|
23
|
+
return true;
|
|
24
|
+
try {
|
|
25
|
+
process.kill(marker.processId, 0);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (cause) {
|
|
29
|
+
return cause.code === "EPERM";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function refuseWhileLanding(state) {
|
|
33
|
+
if (landingMarkerLive(state.landing))
|
|
34
|
+
throw new AgentOpsError("RUN_LANDING_IN_PROGRESS", `Run ${state.runId} is landing delivered work; wait for run land to finish.`);
|
|
35
|
+
}
|
|
13
36
|
const RUN_ID = /^[a-z][a-z0-9-]{7,63}$/u;
|
|
14
37
|
const ID = /^[A-Za-z0-9._:/-]{1,256}$/u;
|
|
15
38
|
const SHA = /^[a-f0-9]{40,64}$/u;
|
|
@@ -127,6 +150,15 @@ export function assertRunState(value) {
|
|
|
127
150
|
return invalid("Run phase or task progress is invalid.");
|
|
128
151
|
if (state.policyBinding !== undefined && !validPolicyBinding(state.policyBinding))
|
|
129
152
|
return invalid("Invalid run policy binding.");
|
|
153
|
+
if (state.landings !== undefined && (!boundedArray(state.landings, 512) || state.landings.some(l => !plain(l) ||
|
|
154
|
+
!boundedArray(l.taskIds, 512) || l.taskIds.length === 0 || l.taskIds.some(id => !ID.test(String(id))) ||
|
|
155
|
+
typeof l.receipt !== "string" || l.receipt.length === 0 || l.receipt.length > 4096 ||
|
|
156
|
+
!SHA.test(String(l.target)) || !Number.isFinite(Date.parse(String(l.landedAt))))))
|
|
157
|
+
return invalid("Invalid run landing record.");
|
|
158
|
+
if (state.landing != null && (!plain(state.landing) || !Number.isSafeInteger(state.landing.processId) ||
|
|
159
|
+
state.landing.processId < 1 || typeof state.landing.hostname !== "string" || state.landing.hostname.length > 256 ||
|
|
160
|
+
!Number.isFinite(Date.parse(String(state.landing.startedAt)))))
|
|
161
|
+
return invalid("Invalid run landing marker.");
|
|
130
162
|
if (state.proofProcess != null && (!plain(state.proofProcess) || !Number.isSafeInteger(state.proofProcess.processId) ||
|
|
131
163
|
state.proofProcess.processId < 1 || typeof state.proofProcess.processIdentity !== "string" ||
|
|
132
164
|
state.proofProcess.processIdentity.length === 0 || state.proofProcess.processIdentity.length > 256))
|
|
@@ -442,6 +474,7 @@ export class RunService {
|
|
|
442
474
|
}
|
|
443
475
|
async resume(runId, retryApprovalIds = [], upgradeBudget = true, retryDeliveryTaskId, expectedFailureId) {
|
|
444
476
|
const state = await this.#repository.mutate(runId, async (current) => {
|
|
477
|
+
refuseWhileLanding(current);
|
|
445
478
|
if (retryDeliveryTaskId !== undefined && retryApprovalIds.length > 0)
|
|
446
479
|
throw new AgentOpsError("RUN_DELIVERY_RETRY_INVALID", "Delivery and approval retries require separate explicit controls.");
|
|
447
480
|
const retry = retryDeliveryTaskId === undefined ? null : validateDeliveryRetry(current, retryDeliveryTaskId, Date.parse(this.#now()));
|
|
@@ -491,6 +524,7 @@ export class RunService {
|
|
|
491
524
|
const cleanAnswer = bounded(answer.trim(), "answer", MAX_GOAL_LENGTH);
|
|
492
525
|
const state = await this.#repository.mutate(runId, (current) => {
|
|
493
526
|
const question = current.questions.find((item) => item.questionId === cleanQuestion);
|
|
527
|
+
refuseWhileLanding(current);
|
|
494
528
|
if (current.status !== "awaiting-input" || current.disableRestart)
|
|
495
529
|
throw new AgentOpsError("RUN_NOT_AWAITING_INPUT", "Respond cannot restart a stopped or terminal run.");
|
|
496
530
|
if (question === undefined)
|
|
@@ -174,7 +174,8 @@ removes only `run` and keeps `loop`.
|
|
|
174
174
|
ends without completing (blocked, budget exhausted, stopped, failing review)
|
|
175
175
|
is reported with its `run status` state and code, its last `run logs` events,
|
|
176
176
|
and the exact `agent-ops run resume <id>` and `agent-ops run stop <id>`
|
|
177
|
-
commands,
|
|
177
|
+
commands, plus `agent-ops run land <id> --yes` when it has delivered tasks,
|
|
178
|
+
and is never resumed or landed automatically or taken over by the session.
|
|
178
179
|
With worktree auto mode, the Claude `PreToolUse` worktree guard allows a
|
|
179
180
|
write to exactly `.agent-ops/state/run-goal.md` in the main checkout without
|
|
180
181
|
creating a worktree. Confirmed init/update pre-authorizes `agent-ops run`:
|
|
@@ -73,6 +73,19 @@
|
|
|
73
73
|
Interrupted controlled proof MUST record bounded redacted exit/signal, result
|
|
74
74
|
failure stage and subprocess diagnostics without treating invalid output or a
|
|
75
75
|
nonzero successful envelope as proof.
|
|
76
|
+
- `run land <id> [--task <task-id>]... --yes` MAY merge delivered children of a
|
|
77
|
+
paused, blocked or budget-limited run with no live worker ahead of the rest.
|
|
78
|
+
Every selected task's dependencies MUST be selected or already landed
|
|
79
|
+
(`RUN_LAND_DEPENDENCY_OPEN`). Proof runs in a fresh landing worktree from the
|
|
80
|
+
target under a generated root task (required verifiers plus a review-only
|
|
81
|
+
coherence criterion, the run root's surface), then the usual verification,
|
|
82
|
+
two-round tree review and receipt-bound finish. The run's checkouts are pinned
|
|
83
|
+
and never removed; landed tasks stay delivered, the run stays resumable and
|
|
84
|
+
records `landings[]` and `RUN_PARTIAL_LANDED`, and its final proof re-verifies
|
|
85
|
+
the whole goal on the new target. While landing, a marker refuses resume,
|
|
86
|
+
respond and another land (`RUN_LANDING_IN_PROGRESS`); a marker of a process
|
|
87
|
+
that no longer exists does not block. A failure leaves the target unchanged
|
|
88
|
+
and keeps the landing worktree until the next land removes it.
|
|
76
89
|
Initial recovery after a current manual resume MAY ignore an owned Codex limit
|
|
77
90
|
snapshot received before activation whose native update second strictly predates
|
|
78
91
|
that resume. Automatic restarts, unknown/equal/future timestamps, clock rollback,
|
|
@@ -150,8 +150,8 @@ byte-identical。Update MUST 保留此 profile,除非提供 `--auto-run on|off
|
|
|
150
150
|
一行說明該 code,並退回 subtask flow;`RUN_REPO_UNTRUSTED` 則停止並詢問使用者。
|
|
151
151
|
未完成即結束的 run(blocked、budget exhausted、stopped、review 失敗)須回報其
|
|
152
152
|
`run status` state 與 code、最後的 `run logs` event,以及確切的
|
|
153
|
-
`agent-ops run resume <id>` 與 `agent-ops run stop <id>` command
|
|
154
|
-
resume 或由 session 接手。在 worktree auto mode 下,Claude `PreToolUse`
|
|
153
|
+
`agent-ops run resume <id>` 與 `agent-ops run stop <id>` command;有已交付 task
|
|
154
|
+
時另附 `agent-ops run land <id> --yes`,且永不自動 resume、land 或由 session 接手。在 worktree auto mode 下,Claude `PreToolUse`
|
|
155
155
|
worktree guard 允許在 main checkout 寫入恰為 `.agent-ops/state/run-goal.md`
|
|
156
156
|
的路徑,且不建立 worktree。確認後的 init/update 會預先授權 `agent-ops run`:
|
|
157
157
|
Claude Code 為 `Bash(agent-ops run *)`,Codex 為
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- 執行中的 run 僅在原始 repository trust、config、可執行 runtime 與操作範圍完全相同時,自動續期過期的有限授權。續期以 digest 綁定前一版本並保留既有評估,拒絕無效/倒退時鐘;並行 stop、denial 或 scope 變更使續期失敗。權限已變更或撤銷時須沿既有 trust/resume 流程取得真正授權;續期不授予永久 trust,也不消除 denial。明確 resume 先核對不可變 renewal lineage,再同步 policy。回答問題也沿用相同恢復檢查;兩者皆不得復活已被明確拒絕的工作。
|
|
23
23
|
- `run resume <id> --retry-approval <auth-event-id>` MAY 明確重試以額度不足 wrapper 失敗的 Codex 自動審批。每筆指定失敗 MUST 保有原始 private artifact、digest、worker identity 與 policy lineage;真正或未指定的拒絕仍阻擋。多筆失敗可重複旗標。成功確認會留存並在後續 resume 重新驗證;此操作不授予命令權限,原生審批必須重跑,既有 writer 死亡、checkout、trust 與預算檢查仍適用。
|
|
24
24
|
- `run resume <id> --retry-delivery <task-id>` MAY 在修復 harness 原因後,明確重新排程一個被 `RUN_FINAL_PROOF_INTERRUPTED` 阻擋的 child。MUST 核對同一筆中斷 handoff、所有 writer 與 proof 已確認停止、所屬 checkout 乾淨及目前 trust/policy;保留 run、task、goal、baselines、commits、耗用與失敗歷史,記錄已確認的 attempt,並要求新 native generation 及正常交付驗證。MUST NOT 直接標記交付、解除其他阻擋或確認命令拒絕;approval retry 是獨立操作。一般 resume 保留 no-progress 阻擋。受控 proof 中斷 MUST 記錄有界且已遮蔽的 exit/signal、失敗階段與子程序診斷;無效輸出或非零退出的成功 envelope 不得視為證據。
|
|
25
|
+
- `run land <id> [--task <task-id>]... --yes` MAY 在 run 為 paused、blocked 或 budget-limited 且無存活 worker 時,先行合併已交付的 child。每個選取 task 的依賴 MUST 已選取或已落地(`RUN_LAND_DEPENDENCY_OPEN`)。證明在由 target 建立的全新 landing worktree 中進行,使用自動產生的 root task(required verifier 加上 review-only 一致性條件,surface 沿用 run root),再走一般驗證、兩輪 tree review 與綁定 receipt 的 finish。run 的 checkout 只被釘住、永不移除;已落地 task 維持 delivered,run 仍可 resume 並記錄 `landings[]` 與 `RUN_PARTIAL_LANDED`,其最終證明會在新 target 上重新驗證整個 goal。落地期間的標記會拒絕 resume、respond 與另一次 land(`RUN_LANDING_IN_PROGRESS`);已不存在的程序留下的標記不會阻擋。失敗時 target 不變,landing worktree 保留到下次 land 移除。
|
|
25
26
|
- 當次人工 resume 的初始恢復 MAY 忽略啟用前收到、且 native 更新秒數嚴格早於該次 resume 的本執行緒 Codex 額度快照。自動重啟、未知/同秒/未來時間、時鐘倒退,以及啟用開始後收到的所有額度事件仍有效。Native 啟用 MUST 確認 goal 為 active,才能啟動或轉向 turn。人工 resume 可解除衍生的 `RUN_POLICY_DISABLED` task 阻擋,但保留 ID、耗用、歷史及其他產品失敗。
|
|
26
27
|
- 收斂判斷比較實際 per-check 狀態及未解除的 finding pins;source fingerprint、時間或診斷文字本身不能算進展。
|
|
27
28
|
|
package/package.json
CHANGED