@zq-silk/yui 0.9.0 → 0.10.1
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/ARCHITECTURE.md +3 -3
- package/README.md +42 -14
- package/dist/cli/commandCatalog.js +9 -1
- package/dist/cli.js +26 -1
- package/dist/commands/globalRoleCommands.js +0 -12
- package/dist/commands/sessionCommands.js +116 -0
- package/dist/commands/taskBaseCommands.js +1 -11
- package/dist/commands/taskCommands.js +19 -39
- package/dist/controller/clientRuntime.js +65 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2 -49
- package/dist/doctor/doctor.js +16 -12
- package/dist/executor/agentAdapter.js +39 -42
- package/dist/executor/codexConfigConflict.js +40 -16
- package/dist/executor/fileRoleLaunchPlanner.js +15 -24
- package/dist/integration/deliveryObligation.js +72 -0
- package/dist/observability/orchestrationMetrics.js +7 -7
- package/dist/repository/taskBaseFreshness.js +5 -11
- package/dist/role/role.js +0 -9
- package/dist/runtime/{firstProgressStopLoss.js → firstProgressAdvisory.js} +7 -21
- package/dist/runtime/tmuxAdapters.js +8 -2
- package/dist/scheduler/leaderWakeupProcessor.js +0 -25
- package/dist/setup/setupCommand.js +0 -5
- package/dist/storage/sqliteStore.js +1 -0
- package/dist/storage/taskStore.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +35 -7
- package/dist/task/completionReadiness.js +17 -9
- package/dist/task/nextAction.js +25 -19
- package/i18n/README.zh-CN.md +17 -7
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -9
- package/skills/yui-operator/SKILL.md +7 -7
|
@@ -3,7 +3,6 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
3
3
|
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
4
4
|
import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
|
|
6
|
-
import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
7
6
|
import { hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
8
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
9
8
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
@@ -1300,40 +1299,6 @@ export class FileSchedulerStoreAdapter {
|
|
|
1300
1299
|
clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
|
|
1301
1300
|
getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
|
|
1302
1301
|
getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
|
|
1303
|
-
saveLeaderFirstProgressStopLoss(input) {
|
|
1304
|
-
return this.store.transaction((store) => {
|
|
1305
|
-
const task = store.getTask(input.taskId);
|
|
1306
|
-
const role = store.getRole(input.taskId, input.roleName);
|
|
1307
|
-
if (task === null || task.status !== "active" || role === null
|
|
1308
|
-
|| store.getLeaderFailure(input.taskId) !== null) {
|
|
1309
|
-
return "state-changed";
|
|
1310
|
-
}
|
|
1311
|
-
const stopLoss = projectFirstProgressStopLoss({
|
|
1312
|
-
sessions: store.getTaskRoleSessionSet(input.taskId, input.roleName),
|
|
1313
|
-
events: store.listEvents(input.taskId),
|
|
1314
|
-
workItems: store.listWorkItems(input.taskId),
|
|
1315
|
-
reviewRounds: store.listReviewRounds(input.taskId),
|
|
1316
|
-
integrations: store.listIntegrationAttempts(input.taskId)
|
|
1317
|
-
});
|
|
1318
|
-
if (!stopLoss.exhausted || stopLoss.fingerprint !== input.expectedFingerprint) {
|
|
1319
|
-
return "state-changed";
|
|
1320
|
-
}
|
|
1321
|
-
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1322
|
-
const lastSession = sessions === null
|
|
1323
|
-
? undefined
|
|
1324
|
-
: [...(sessions.history ?? []), ...Object.values(sessions.sessions)]
|
|
1325
|
-
.sort((left, right) => left.createdAt.localeCompare(right.createdAt))
|
|
1326
|
-
.at(-1);
|
|
1327
|
-
const message = `Leader first-progress stop-loss: ${stopLoss.reason}`;
|
|
1328
|
-
store.saveRole(input.taskId, updateRoleStatus(role, "failed", input.now));
|
|
1329
|
-
store.saveLeaderFailure(recordLeaderFailure(input.taskId, lastSession?.nativeSessionId ?? "(unregistered)", message, input.now, null));
|
|
1330
|
-
store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, input.now, store.getOperatorNotification(input.taskId)));
|
|
1331
|
-
enqueueWork(store, { kind: "operator" }, "leader-first-progress-stop-loss", input.now, [
|
|
1332
|
-
{ type: "task", id: input.taskId }
|
|
1333
|
-
]);
|
|
1334
|
-
return "recorded";
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
1302
|
saveLeaderDispatch(input) {
|
|
1338
1303
|
return this.store.transaction((store) => {
|
|
1339
1304
|
const task = store.getTask(input.task.id);
|
|
@@ -2287,7 +2252,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2287
2252
|
failedNativeTurnId: input.nativeTurnId,
|
|
2288
2253
|
lastErrorSummary: summary,
|
|
2289
2254
|
...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
|
|
2290
|
-
}, now, configuredProviderRetryPolicy(store
|
|
2255
|
+
}, now, configuredProviderRetryPolicy(store));
|
|
2291
2256
|
if (retryDecision.outcome === "exhausted") {
|
|
2292
2257
|
this.recordProviderRetryClassified(store, input, classification.errorClass, {
|
|
2293
2258
|
wouldRetry: "false",
|
|
@@ -4069,19 +4034,7 @@ function compareCanonicalObservationOrder(left, right) {
|
|
|
4069
4034
|
|| (left.ordinal ?? -1) - (right.ordinal ?? -1)
|
|
4070
4035
|
|| left.eventId.localeCompare(right.eventId);
|
|
4071
4036
|
}
|
|
4072
|
-
function configuredProviderRetryPolicy(store
|
|
4037
|
+
function configuredProviderRetryPolicy(store) {
|
|
4073
4038
|
const config = providerRetryConfig(store.getConfig());
|
|
4074
|
-
if (taskId !== undefined && roleName === "leader") {
|
|
4075
|
-
const progress = projectFirstProgressStopLoss({
|
|
4076
|
-
sessions: store.getTaskRoleSessionSet(taskId, roleName),
|
|
4077
|
-
events: store.listEvents(taskId),
|
|
4078
|
-
workItems: store.listWorkItems(taskId),
|
|
4079
|
-
reviewRounds: store.listReviewRounds(taskId),
|
|
4080
|
-
integrations: store.listIntegrationAttempts(taskId)
|
|
4081
|
-
});
|
|
4082
|
-
if (progress.firstProgressAt === undefined) {
|
|
4083
|
-
return boundProviderRetryBeforeFirstProgress(config, progress);
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
4039
|
return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
|
|
4087
4040
|
}
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ import Database from "better-sqlite3";
|
|
|
4
4
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
5
5
|
import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
|
|
6
6
|
import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
|
|
7
|
-
import { inspectCodexLaunchConfig } from "../executor/codexConfigConflict.js";
|
|
7
|
+
import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "../executor/codexConfigConflict.js";
|
|
8
8
|
import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
|
|
9
9
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
10
10
|
import { compileRoleSessionContext } from "../context/roleSessionContext.js";
|
|
@@ -789,14 +789,8 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
|
|
|
789
789
|
trustWorkspace: true
|
|
790
790
|
})
|
|
791
791
|
: undefined;
|
|
792
|
-
if (codexConfig
|
|
793
|
-
|
|
794
|
-
name: "reviewer launch",
|
|
795
|
-
status: "invalid",
|
|
796
|
-
detail: "Codex notify is already configured by "
|
|
797
|
-
+ `${codexConfig.notify.source}; Yui requires exclusive ownership of the structured `
|
|
798
|
-
+ "notify callback and refuses to replace or be replaced by native configuration."
|
|
799
|
-
};
|
|
792
|
+
if (codexConfig !== undefined) {
|
|
793
|
+
assertCodexLaunchOverridesAvailable(codexConfig, ["developerInstructions", "notify"]);
|
|
800
794
|
}
|
|
801
795
|
const reviewerContext = adapter.id === "codex"
|
|
802
796
|
? compileRoleSessionContext(home, role, { scope: "global" }, { purpose: "review" })
|
|
@@ -810,8 +804,7 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
|
|
|
810
804
|
? {}
|
|
811
805
|
: {
|
|
812
806
|
developerInstructions: reviewerContext.developerInstructions,
|
|
813
|
-
skills: reviewerContext.skills
|
|
814
|
-
codexDeveloperInstructions: codexConfig?.developerInstructions
|
|
807
|
+
skills: reviewerContext.skills
|
|
815
808
|
})
|
|
816
809
|
});
|
|
817
810
|
if (compiled.argv.length === 0)
|
|
@@ -819,7 +812,18 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
|
|
|
819
812
|
return {
|
|
820
813
|
name: "reviewer launch",
|
|
821
814
|
status: "ok",
|
|
822
|
-
detail:
|
|
815
|
+
detail: [
|
|
816
|
+
`adapter=${adapter.id}`,
|
|
817
|
+
`strategy=${compiled.sessionStrategy}`,
|
|
818
|
+
`command=${agent.command}`,
|
|
819
|
+
`addDirs=${launchConfig.additionalDirectories?.length ?? 0}`,
|
|
820
|
+
...(codexConfig?.developerInstructions.status === "configured"
|
|
821
|
+
? [`override=developer_instructions@${codexConfig.developerInstructions.source}`]
|
|
822
|
+
: []),
|
|
823
|
+
...(codexConfig?.notify.status === "configured"
|
|
824
|
+
? [`override=notify@${codexConfig.notify.source}`]
|
|
825
|
+
: [])
|
|
826
|
+
].join(" ")
|
|
823
827
|
};
|
|
824
828
|
}
|
|
825
829
|
catch (error) {
|
|
@@ -4,7 +4,6 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
4
4
|
import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
|
|
5
5
|
import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgentBaseArguments } from "../agent/argumentPolicy.js";
|
|
6
6
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
7
|
-
import { inspectCodexDeveloperInstructions } from "./codexConfigConflict.js";
|
|
8
7
|
import { discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
|
|
9
8
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
10
9
|
const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
|
|
@@ -139,16 +138,6 @@ class CodexAdapter extends BaseAdapter {
|
|
|
139
138
|
];
|
|
140
139
|
if (instructions.length === 0)
|
|
141
140
|
return workspaceTrust;
|
|
142
|
-
const nativeInstructions = input.codexDeveloperInstructions
|
|
143
|
-
?? inspectCodexDeveloperInstructions({
|
|
144
|
-
workspace: input.workspace,
|
|
145
|
-
profile: input.config.profile,
|
|
146
|
-
trustWorkspace: true
|
|
147
|
-
});
|
|
148
|
-
if (nativeInstructions.status === "configured") {
|
|
149
|
-
throw new Error("Codex developer_instructions is already configured by "
|
|
150
|
-
+ `${nativeInstructions.source}; Yui refuses to replace native developer instructions.`);
|
|
151
|
-
}
|
|
152
141
|
return [
|
|
153
142
|
...workspaceTrust,
|
|
154
143
|
"--config",
|
|
@@ -375,37 +364,33 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
|
|
|
375
364
|
if (!supported) {
|
|
376
365
|
return snapshot(agent, adapter, {
|
|
377
366
|
status: "unsupported-version", command: agent.command, version,
|
|
378
|
-
reason: adapter.
|
|
379
|
-
? `Minimum supported version is ${adapter.supportedVersion}.`
|
|
380
|
-
: `Supported version line starts at ${adapter.supportedVersion}.`,
|
|
367
|
+
reason: `Minimum supported version is ${adapter.supportedVersion}.`,
|
|
381
368
|
probedAt: at
|
|
382
369
|
}, fields, at, [`Installed version ${version} is not supported by adapter ${adapter.id}.`]);
|
|
383
370
|
}
|
|
384
371
|
const help = run(agent.command, ["--help"]);
|
|
385
372
|
const helpFailure = failed(help);
|
|
386
|
-
if (
|
|
373
|
+
if (helpFailure !== undefined) {
|
|
387
374
|
return snapshot(agent, adapter, {
|
|
388
375
|
status: "probe-failed", command: agent.command, version,
|
|
389
|
-
reason: `Required
|
|
376
|
+
reason: `Required ${adapter.label} capability probe failed: ${helpFailure}`,
|
|
377
|
+
probedAt: at
|
|
390
378
|
}, fields, at);
|
|
391
379
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
+ `${CODEX_TESTED_THROUGH_VERSION}; required capabilities were detected.`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
380
|
+
const helpOutput = output(help.stdout, help.stderr);
|
|
381
|
+
fields = fromHelp(agent.adapterId, helpOutput);
|
|
382
|
+
const missing = missingRequiredCapabilities(adapter.id, helpOutput);
|
|
383
|
+
if (missing.length > 0) {
|
|
384
|
+
return snapshot(agent, adapter, {
|
|
385
|
+
status: "unsupported-version", command: agent.command, version,
|
|
386
|
+
reason: `${adapter.label} CLI is missing required capabilities: ${missing.join(", ")}.`,
|
|
387
|
+
probedAt: at
|
|
388
|
+
}, fields, at);
|
|
389
|
+
}
|
|
390
|
+
if (adapter.id === "codex"
|
|
391
|
+
&& compareVersions(version, CODEX_TESTED_THROUGH_VERSION) > 0) {
|
|
392
|
+
warnings.push(`Installed Codex version ${version} is newer than the latest tested version `
|
|
393
|
+
+ `${CODEX_TESTED_THROUGH_VERSION}; required capabilities were detected.`);
|
|
409
394
|
}
|
|
410
395
|
return snapshot(agent, adapter, {
|
|
411
396
|
status: "installed", command: agent.command, version, probedAt: at
|
|
@@ -498,10 +483,7 @@ function output(stdout, stderr) {
|
|
|
498
483
|
return value;
|
|
499
484
|
}
|
|
500
485
|
function supports(version, adapter) {
|
|
501
|
-
|
|
502
|
-
return compareVersions(version, adapter.supportedVersion) >= 0;
|
|
503
|
-
const left = version.split(".").map(Number), right = adapter.supportedVersion.split(".").map(Number);
|
|
504
|
-
return left[0] === right[0] && left[1] === right[1] && left[2] >= right[2];
|
|
486
|
+
return compareVersions(version, adapter.supportedVersion) >= 0;
|
|
505
487
|
}
|
|
506
488
|
function compareVersions(leftVersion, rightVersion) {
|
|
507
489
|
const left = leftVersion.split(".").map(Number);
|
|
@@ -513,11 +495,26 @@ function compareVersions(leftVersion, rightVersion) {
|
|
|
513
495
|
}
|
|
514
496
|
return 0;
|
|
515
497
|
}
|
|
516
|
-
function
|
|
517
|
-
const required =
|
|
518
|
-
[
|
|
519
|
-
|
|
520
|
-
|
|
498
|
+
function missingRequiredCapabilities(id, help) {
|
|
499
|
+
const required = id === "codex"
|
|
500
|
+
? [
|
|
501
|
+
[/(?:^|\s)--config(?:\s|[=<,]|$)/m, "--config"],
|
|
502
|
+
[/^\s*resume(?:\s|$)/m, "resume"]
|
|
503
|
+
]
|
|
504
|
+
: [
|
|
505
|
+
[/(?:^|\s)--append-system-prompt(?:-file|\[-file\])(?:\s|[=<,]|$)/m,
|
|
506
|
+
"--append-system-prompt-file"],
|
|
507
|
+
[/(?:^|\s)--resume(?:\s|[=<,]|$)/m, "--resume"],
|
|
508
|
+
[/(?:^|\s)--session-id(?:\s|[=<,]|$)/m, "--session-id"],
|
|
509
|
+
[/(?:^|\s)-p(?:\s|[=<,]|$)/m, "-p"],
|
|
510
|
+
[/(?:^|\s)--output-format(?:\s|[=<,]|$)/m, "--output-format"],
|
|
511
|
+
[/(?:^|\s)--input-format(?:\s|[=<,]|$)/m, "--input-format"],
|
|
512
|
+
[/(?:^|\s)--verbose(?:\s|[=<,]|$)/m, "--verbose"],
|
|
513
|
+
[/(?:^|\s)--replay-user-messages(?:\s|[=<,]|$)/m,
|
|
514
|
+
"--replay-user-messages"],
|
|
515
|
+
[/(?:^|\s)--plugin-dir(?:\s|[=<,]|$)/m, "--plugin-dir"],
|
|
516
|
+
[/(?:^|\s)--name(?:\s|[=<,]|$)/m, "--name"]
|
|
517
|
+
];
|
|
521
518
|
return required.flatMap(([pattern, label]) => pattern.test(help) ? [] : [label]);
|
|
522
519
|
}
|
|
523
520
|
function cloneConfig(config, paths) {
|
|
@@ -4,15 +4,14 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import { parse } from "smol-toml";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* layers
|
|
10
|
-
*
|
|
7
|
+
* Reports the local, file-backed Codex configuration layers supported by Yui.
|
|
8
|
+
* Yui's invocation-local launch settings take precedence over ordinary local
|
|
9
|
+
* layers without mutating those files. Higher-precedence managed policy is
|
|
10
|
+
* reported separately because an invocation cannot override it. Remote
|
|
11
|
+
* managed layers remain outside this reporting boundary. Project files are
|
|
12
|
+
* considered only when the directory, project root, or repository root is
|
|
13
|
+
* trusted.
|
|
11
14
|
*/
|
|
12
|
-
export function inspectCodexDeveloperInstructions(input) {
|
|
13
|
-
return inspectCodexConfigKeys(input, ["developer_instructions"])
|
|
14
|
-
.developerInstructions;
|
|
15
|
-
}
|
|
16
15
|
export function inspectCodexLaunchConfig(input) {
|
|
17
16
|
return inspectCodexConfigKeys(input, ["developer_instructions", "notify"]);
|
|
18
17
|
}
|
|
@@ -49,12 +48,17 @@ function inspectCodexConfigKeys(input, keys) {
|
|
|
49
48
|
? withExactWorkspaceTrust(discovery, input.workspace)
|
|
50
49
|
: discovery);
|
|
51
50
|
const candidates = [
|
|
52
|
-
...discoveryPaths.map((path) => ({
|
|
51
|
+
...discoveryPaths.map((path) => ({
|
|
52
|
+
path,
|
|
53
|
+
keys,
|
|
54
|
+
precedence: "session-overridable"
|
|
55
|
+
})),
|
|
53
56
|
...projectPaths.map((path) => ({
|
|
54
57
|
path,
|
|
55
|
-
keys: keys.filter((key) => key === "developer_instructions")
|
|
58
|
+
keys: keys.filter((key) => key === "developer_instructions"),
|
|
59
|
+
precedence: "session-overridable"
|
|
56
60
|
})),
|
|
57
|
-
{ path: managedPath, keys }
|
|
61
|
+
{ path: managedPath, keys, precedence: "managed" }
|
|
58
62
|
];
|
|
59
63
|
let developerInstructions = { status: "absent" };
|
|
60
64
|
let notify = { status: "absent" };
|
|
@@ -71,16 +75,36 @@ function inspectCodexConfigKeys(input, keys) {
|
|
|
71
75
|
catch (error) {
|
|
72
76
|
throw unreliableInspection(candidate.path, error);
|
|
73
77
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
if (configured.has("developer_instructions")) {
|
|
79
|
+
developerInstructions = {
|
|
80
|
+
status: "configured",
|
|
81
|
+
source: candidate.path,
|
|
82
|
+
precedence: candidate.precedence
|
|
83
|
+
};
|
|
77
84
|
}
|
|
78
|
-
if (
|
|
79
|
-
notify = {
|
|
85
|
+
if (configured.has("notify")) {
|
|
86
|
+
notify = {
|
|
87
|
+
status: "configured",
|
|
88
|
+
source: candidate.path,
|
|
89
|
+
precedence: candidate.precedence
|
|
90
|
+
};
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
return { developerInstructions, notify };
|
|
83
94
|
}
|
|
95
|
+
export function assertCodexLaunchOverridesAvailable(inspection, keys) {
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const configured = inspection[key];
|
|
98
|
+
if (configured.status !== "configured" || configured.precedence !== "managed") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const nativeKey = key === "developerInstructions"
|
|
102
|
+
? "developer_instructions"
|
|
103
|
+
: "notify";
|
|
104
|
+
throw new Error(`Codex ${nativeKey} is controlled by higher-precedence managed configuration at `
|
|
105
|
+
+ `${configured.source}; Yui cannot apply its invocation-local ${nativeKey} value.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
84
108
|
function withExactWorkspaceTrust(discovery, workspace) {
|
|
85
109
|
return {
|
|
86
110
|
...discovery,
|
|
@@ -13,7 +13,6 @@ import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from
|
|
|
13
13
|
import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
|
|
14
14
|
import { prefixYuiTitleInput } from "../run/runIdentity.js";
|
|
15
15
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
16
|
-
import { inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
17
16
|
import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
18
17
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
19
18
|
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
@@ -26,6 +25,7 @@ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
|
26
25
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
27
26
|
import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
28
27
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
28
|
+
import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
29
29
|
/** Builds managed native Agent launches from the authoritative Task records. */
|
|
30
30
|
export class FileRoleLaunchPlanner {
|
|
31
31
|
home;
|
|
@@ -274,6 +274,19 @@ export class FileRoleLaunchPlanner {
|
|
|
274
274
|
const adapter = resolveAgentAdapter(binding.adapterId);
|
|
275
275
|
const effectiveWorkspace = effective.workspace.root;
|
|
276
276
|
const agentWorkspace = nativeAgentWorkspace(effective.workspace);
|
|
277
|
+
if (adapter.id === "codex") {
|
|
278
|
+
const codexConfig = inspectCodexLaunchConfig({
|
|
279
|
+
environment: launchEnvironment,
|
|
280
|
+
workspace: agentWorkspace,
|
|
281
|
+
profile: binding.config.adapterId === "codex"
|
|
282
|
+
? binding.config.profile
|
|
283
|
+
: undefined,
|
|
284
|
+
trustWorkspace: true
|
|
285
|
+
});
|
|
286
|
+
assertCodexLaunchOverridesAvailable(codexConfig, owner.scope !== "task" || input.runId === undefined
|
|
287
|
+
? ["developerInstructions", "notify"]
|
|
288
|
+
: ["developerInstructions"]);
|
|
289
|
+
}
|
|
277
290
|
const runtimeIsolation = input.runtimeIsolation === undefined
|
|
278
291
|
? undefined
|
|
279
292
|
: parseTaskRuntimeIsolationDescriptor(JSON.stringify(input.runtimeIsolation));
|
|
@@ -309,25 +322,6 @@ export class FileRoleLaunchPlanner {
|
|
|
309
322
|
sessionManifestDigest: bootstrap.manifest.digest,
|
|
310
323
|
sessionCliPath: bootstrap.sessionCliPath
|
|
311
324
|
};
|
|
312
|
-
const codexConfig = binding.config.adapterId === "codex"
|
|
313
|
-
? inspectCodexLaunchConfig({
|
|
314
|
-
environment: {
|
|
315
|
-
...operationalSourceEnvironment,
|
|
316
|
-
...agentSourceEnvironment,
|
|
317
|
-
...launchEnvironment
|
|
318
|
-
},
|
|
319
|
-
workspace: agentWorkspace,
|
|
320
|
-
profile: binding.config.profile,
|
|
321
|
-
trustWorkspace: true
|
|
322
|
-
})
|
|
323
|
-
: undefined;
|
|
324
|
-
if (codexConfig?.notify.status === "configured"
|
|
325
|
-
&& (owner.scope !== "task" || input.runId === undefined)) {
|
|
326
|
-
throw new Error("Codex notify is already configured by "
|
|
327
|
-
+ `${codexConfig.notify.source}; this interactive Yui Session requires exclusive `
|
|
328
|
-
+ "ownership of the structured notify callback and refuses to replace or be replaced "
|
|
329
|
-
+ "by native configuration.");
|
|
330
|
-
}
|
|
331
325
|
const managedRun = owner.scope === "task" && input.runId !== undefined
|
|
332
326
|
? this.store.getAgentRun(owner.taskId, input.runId)
|
|
333
327
|
: null;
|
|
@@ -353,10 +347,7 @@ export class FileRoleLaunchPlanner {
|
|
|
353
347
|
config: effectiveConfig,
|
|
354
348
|
workspace: agentWorkspace,
|
|
355
349
|
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
356
|
-
...sessionContext
|
|
357
|
-
...(codexConfig === undefined
|
|
358
|
-
? {}
|
|
359
|
-
: { codexDeveloperInstructions: codexConfig.developerInstructions })
|
|
350
|
+
...sessionContext
|
|
360
351
|
};
|
|
361
352
|
if (input.mode === "resume"
|
|
362
353
|
&& knownNativeSessionId !== undefined
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
2
|
+
/**
|
|
3
|
+
* Delivery obligations follow the Candidate that currently governs each
|
|
4
|
+
* WorkItem. Older Candidates and their ChangeSets remain audit evidence, but
|
|
5
|
+
* they do not keep a Task open after a replacement Candidate is accepted.
|
|
6
|
+
*/
|
|
7
|
+
export function governingChangeSets(workItems, changeSets) {
|
|
8
|
+
const selected = new Map();
|
|
9
|
+
for (const item of workItems) {
|
|
10
|
+
const candidate = governingWorkItemCandidate(item);
|
|
11
|
+
if (candidate === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
for (const changeSet of changeSets) {
|
|
14
|
+
if (changeSet.workItemId !== item.id)
|
|
15
|
+
continue;
|
|
16
|
+
const expectedHead = candidateProjectHead(candidate, changeSet.projectId);
|
|
17
|
+
if (expectedHead !== undefined && changeSet.headCommit !== expectedHead)
|
|
18
|
+
continue;
|
|
19
|
+
const key = `${item.id}\0${changeSet.projectId}`;
|
|
20
|
+
const current = selected.get(key);
|
|
21
|
+
if (current === undefined || compareChangeSets(current, changeSet) < 0) {
|
|
22
|
+
selected.set(key, changeSet);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return [...selected.values()].sort(compareChangeSets);
|
|
27
|
+
}
|
|
28
|
+
export function changeSetDeliverySettled(changeSet, integrations, queueEntries = []) {
|
|
29
|
+
if (integrations.some((attempt) => (attempt.status === "committed" && attempt.changeSetIds.includes(changeSet.id))))
|
|
30
|
+
return true;
|
|
31
|
+
const latestQueueEntry = queueEntries
|
|
32
|
+
.filter((entry) => entry.changeSetId === changeSet.id)
|
|
33
|
+
.sort(compareQueueEntries)
|
|
34
|
+
.at(-1);
|
|
35
|
+
return latestQueueEntry?.status === "superseded";
|
|
36
|
+
}
|
|
37
|
+
export function latestGoverningQueueEntries(changeSets, queueEntries) {
|
|
38
|
+
const governingIds = new Set(changeSets.map(({ id }) => id));
|
|
39
|
+
const latest = new Map();
|
|
40
|
+
for (const entry of queueEntries) {
|
|
41
|
+
if (!governingIds.has(entry.changeSetId))
|
|
42
|
+
continue;
|
|
43
|
+
const current = latest.get(entry.changeSetId);
|
|
44
|
+
if (current === undefined || compareQueueEntries(current, entry) < 0) {
|
|
45
|
+
latest.set(entry.changeSetId, entry);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [...latest.values()].sort(compareQueueEntries);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Historical blocked attempts are audit evidence once every ChangeSet they
|
|
52
|
+
* reference has been replaced by a newer governing Candidate. Attempts that
|
|
53
|
+
* may still be writing remain blockers regardless of Candidate history.
|
|
54
|
+
*/
|
|
55
|
+
export function integrationAttemptRequiresSettlement(attempt, changeSets) {
|
|
56
|
+
if (attempt.status === "running" || attempt.status === "validating")
|
|
57
|
+
return true;
|
|
58
|
+
const governingIds = new Set(changeSets.map(({ id }) => id));
|
|
59
|
+
return attempt.changeSetIds.some((id) => governingIds.has(id));
|
|
60
|
+
}
|
|
61
|
+
function candidateProjectHead(candidate, projectId) {
|
|
62
|
+
return candidate.gitSnapshot?.projects.find((project) => project.projectId === projectId)?.commit
|
|
63
|
+
?? candidate.taskMainSnapshot?.projects.find((project) => project.projectId === projectId)?.headCommit;
|
|
64
|
+
}
|
|
65
|
+
function compareChangeSets(left, right) {
|
|
66
|
+
return left.createdAt.localeCompare(right.createdAt)
|
|
67
|
+
|| left.id.localeCompare(right.id, undefined, { numeric: true });
|
|
68
|
+
}
|
|
69
|
+
function compareQueueEntries(left, right) {
|
|
70
|
+
return left.updatedAt.localeCompare(right.updatedAt)
|
|
71
|
+
|| left.id.localeCompare(right.id, undefined, { numeric: true });
|
|
72
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
2
|
-
import {
|
|
2
|
+
import { projectFirstProgressAdvisory } from "../runtime/firstProgressAdvisory.js";
|
|
3
3
|
/** One Task's orchestration cost and advisory projection, with no writes. */
|
|
4
4
|
export function projectTaskOrchestration(facts) {
|
|
5
5
|
const evidence = {
|
|
@@ -35,14 +35,14 @@ export function projectTaskOrchestration(facts) {
|
|
|
35
35
|
const repeatedIdentities = [...integrationIdentities.values()]
|
|
36
36
|
.reduce((total, count) => total + Math.max(0, count - 1), 0);
|
|
37
37
|
const leaderSessions = facts.roleSessionSets.find(({ owner }) => owner.roleName === "leader") ?? null;
|
|
38
|
-
const firstProgress =
|
|
38
|
+
const firstProgress = projectFirstProgressAdvisory({
|
|
39
39
|
sessions: leaderSessions,
|
|
40
40
|
events: facts.events,
|
|
41
41
|
workItems: facts.workItems,
|
|
42
42
|
reviewRounds: facts.reviewRounds,
|
|
43
43
|
integrations: facts.integrations
|
|
44
44
|
});
|
|
45
|
-
const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.
|
|
45
|
+
const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.attentionRecommended);
|
|
46
46
|
const publicationAt = facts.task.completedAt === undefined
|
|
47
47
|
? undefined
|
|
48
48
|
: facts.publications
|
|
@@ -86,7 +86,7 @@ export function projectTaskOrchestration(facts) {
|
|
|
86
86
|
advisories
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities,
|
|
89
|
+
function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgressAttention) {
|
|
90
90
|
const result = [];
|
|
91
91
|
if (facts.task.type === "bugfix" && facts.workItems.length > 0) {
|
|
92
92
|
result.push({
|
|
@@ -136,10 +136,10 @@ function projectAdvisories(facts, classifications, fullRounds, repeatedIdentitie
|
|
|
136
136
|
refs: recent.map(({ id }) => `review-round:${id}`)
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
|
-
if (
|
|
139
|
+
if (firstProgressAttention) {
|
|
140
140
|
result.push({
|
|
141
|
-
code: "provider-first-progress-
|
|
142
|
-
reason: "Two fresh Leader generations produced no first durable progress;
|
|
141
|
+
code: "provider-first-progress-advisory",
|
|
142
|
+
reason: "Two fresh Leader generations produced no first durable progress; consider Operator attention before another generation.",
|
|
143
143
|
refs: [facts.task.id]
|
|
144
144
|
});
|
|
145
145
|
}
|
|
@@ -113,23 +113,17 @@ export function assertTaskBaseFreshnessForCompletion(report, options = {}) {
|
|
|
113
113
|
const acceptedPublishedTree = options.acceptedPublishedTreeProjectId === entry.projectId;
|
|
114
114
|
if ((entry.status === "behind" || entry.status === "diverged")
|
|
115
115
|
&& !acceptedPublishedTree) {
|
|
116
|
-
|
|
117
|
-
+
|
|
118
|
-
+ "Safe resolutions are to rebase or merge the Task workspace onto the refreshed remote base, "
|
|
119
|
-
+ "create a clean delivery branch from that base and cherry-pick the Task changes, "
|
|
120
|
-
+ "or ask the Leader to resolve it. Continuing without rebasing is allowed only after explicitly "
|
|
121
|
-
+ "recording the unrelated-diff risk.");
|
|
116
|
+
warnings.push(`Project ${entry.projectId} base is ${entry.status}; the Leader must choose the delivery base `
|
|
117
|
+
+ "and account for any remote-only changes.");
|
|
122
118
|
}
|
|
123
119
|
if (entry.workspaceClean === false) {
|
|
124
120
|
throw usageError(`Task ${report.taskId} Project ${entry.projectId} workspace is dirty; `
|
|
125
121
|
+ "commit, integrate, or clean it before completion.");
|
|
126
122
|
}
|
|
127
123
|
if (entry.status === "unknown") {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
warnings.push(`Project ${entry.projectId} remote base is unknown; local completion continues without a remote claim.`);
|
|
124
|
+
warnings.push(report.refreshed
|
|
125
|
+
? `Project ${entry.projectId} remote base could not be refreshed: ${entry.error ?? "unknown error"}; local completion continues without a remote claim.`
|
|
126
|
+
: `Project ${entry.projectId} remote base is unknown; local completion continues without a remote claim.`);
|
|
133
127
|
}
|
|
134
128
|
}
|
|
135
129
|
return warnings;
|
package/dist/role/role.js
CHANGED
|
@@ -217,20 +217,11 @@ function validateRoleOwner(role) {
|
|
|
217
217
|
const entries = Object.entries(role.agentBindings);
|
|
218
218
|
if (entries.length === 0)
|
|
219
219
|
throw new Error("Role requires at least one Agent binding.");
|
|
220
|
-
const operatorAdapters = new Map();
|
|
221
220
|
for (const [agentId, binding] of entries) {
|
|
222
221
|
validateRoleAgentBinding(binding);
|
|
223
222
|
if (agentId !== binding.agentId) {
|
|
224
223
|
throw new Error(`Role Agent binding identity is inconsistent: ${agentId}.`);
|
|
225
224
|
}
|
|
226
|
-
if (!("taskId" in role) && role.name === "operator") {
|
|
227
|
-
const existingAgentId = operatorAdapters.get(binding.adapterId);
|
|
228
|
-
if (existingAgentId !== undefined) {
|
|
229
|
-
throw new Error("Operator supports one Agent per adapter; "
|
|
230
|
-
+ `${binding.adapterId} is bound to both ${existingAgentId} and ${agentId}.`);
|
|
231
|
-
}
|
|
232
|
-
operatorAdapters.set(binding.adapterId, agentId);
|
|
233
|
-
}
|
|
234
225
|
}
|
|
235
226
|
if (!Object.hasOwn(role.agentBindings, role.activeAgentId)) {
|
|
236
227
|
throw new Error(`Role active Agent is not bound: ${role.activeAgentId}.`);
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
/** One automatic same-Session continuation is allowed before first progress. */
|
|
3
|
-
export function boundProviderRetryBeforeFirstProgress(policy, projection) {
|
|
4
|
-
return projection.firstProgressAt === undefined
|
|
5
|
-
? Object.freeze({ delaysMs: policy.delaysMs.slice(0, 1), maxWindowMs: policy.maxWindowMs })
|
|
6
|
-
: policy;
|
|
7
|
-
}
|
|
8
|
-
export function projectFirstProgressStopLoss(input) {
|
|
1
|
+
export function projectFirstProgressAdvisory(input) {
|
|
9
2
|
const sessions = input.sessions === null
|
|
10
3
|
? []
|
|
11
4
|
: [...(input.sessions.history ?? []), ...Object.values(input.sessions.sessions)]
|
|
@@ -31,24 +24,17 @@ export function projectFirstProgressStopLoss(input) {
|
|
|
31
24
|
.sort((left, right) => left.at.localeCompare(right.at) || left.ref.localeCompare(right.ref));
|
|
32
25
|
const firstProgressAt = progress[0]?.at;
|
|
33
26
|
const generationsBeforeFirstProgress = unique.filter((session) => (firstProgressAt === undefined || session.createdAt <= firstProgressAt)).length;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const exhausted = firstProgressAt === undefined && generationsBeforeFirstProgress >= 2;
|
|
37
|
-
const fingerprint = createHash("sha256")
|
|
38
|
-
.update(JSON.stringify({ generationRefs, progressRefs }))
|
|
39
|
-
.digest("hex");
|
|
27
|
+
const attentionRecommended = firstProgressAt === undefined
|
|
28
|
+
&& generationsBeforeFirstProgress >= 2;
|
|
40
29
|
return Object.freeze({
|
|
41
|
-
|
|
30
|
+
attentionRecommended,
|
|
42
31
|
generationsBeforeFirstProgress,
|
|
43
32
|
...(firstGenerationAt === undefined ? {} : { firstGenerationAt }),
|
|
44
33
|
...(firstProgressAt === undefined ? {} : { firstProgressAt }),
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
fingerprint,
|
|
48
|
-
reason: exhausted
|
|
49
|
-
? `${generationsBeforeFirstProgress} fresh Leader generations produced no first durable progress; stop before creating another generation and hand off to the Operator.`
|
|
34
|
+
reason: attentionRecommended
|
|
35
|
+
? `${generationsBeforeFirstProgress} fresh Leader generations produced no first durable progress; Operator attention may be useful before another generation.`
|
|
50
36
|
: firstProgressAt !== undefined
|
|
51
37
|
? `First durable progress was recorded at ${firstProgressAt}.`
|
|
52
|
-
:
|
|
38
|
+
: "Fewer than two Leader generations exist before first durable progress."
|
|
53
39
|
});
|
|
54
40
|
}
|