@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.
@@ -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, input.taskId, input.roleName));
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, taskId, roleName) {
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
  }
@@ -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?.notify.status === "configured") {
793
- return {
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: `adapter=${adapter.id} strategy=${compiled.sessionStrategy} command=${agent.command} addDirs=${launchConfig.additionalDirectories?.length ?? 0}`
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.id === "codex"
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 (adapter.id === "codex" && helpFailure !== undefined) {
373
+ if (helpFailure !== undefined) {
387
374
  return snapshot(agent, adapter, {
388
375
  status: "probe-failed", command: agent.command, version,
389
- reason: `Required Codex capability probe failed: ${helpFailure}`, probedAt: at
376
+ reason: `Required ${adapter.label} capability probe failed: ${helpFailure}`,
377
+ probedAt: at
390
378
  }, fields, at);
391
379
  }
392
- if (helpFailure === undefined) {
393
- const helpOutput = output(help.stdout, help.stderr);
394
- fields = fromHelp(agent.adapterId, helpOutput);
395
- if (adapter.id === "codex") {
396
- const missing = missingCodexCapabilities(helpOutput);
397
- if (missing.length > 0) {
398
- return snapshot(agent, adapter, {
399
- status: "unsupported-version", command: agent.command, version,
400
- reason: `Codex CLI is missing required capabilities: ${missing.join(", ")}.`,
401
- probedAt: at
402
- }, fields, at);
403
- }
404
- if (compareVersions(version, CODEX_TESTED_THROUGH_VERSION) > 0) {
405
- warnings.push(`Installed Codex version ${version} is newer than the latest tested version `
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
- if (adapter.id === "codex")
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 missingCodexCapabilities(help) {
517
- const required = [
518
- [/(?:^|\s)--config(?:\s|[=<,]|$)/m, "--config"],
519
- [/^\s*resume(?:\s|$)/m, "resume"]
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
- * Inspects the local, file-backed Codex configuration layers supported by Yui
8
- * that can conflict with launch-owned settings. Remote and platform-managed
9
- * layers are outside this compatibility boundary. Project files are considered
10
- * only when the directory, project root, or project root is trusted.
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) => ({ path, keys })),
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 (developerInstructions.status === "absent"
75
- && configured.has("developer_instructions")) {
76
- developerInstructions = { status: "configured", source: candidate.path };
78
+ if (configured.has("developer_instructions")) {
79
+ developerInstructions = {
80
+ status: "configured",
81
+ source: candidate.path,
82
+ precedence: candidate.precedence
83
+ };
77
84
  }
78
- if (notify.status === "absent" && configured.has("notify")) {
79
- notify = { status: "configured", source: candidate.path };
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 { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
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 = projectFirstProgressStopLoss({
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.exhausted);
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, stopLoss) {
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 (stopLoss) {
139
+ if (firstProgressAttention) {
140
140
  result.push({
141
- code: "provider-first-progress-stop-loss",
142
- reason: "Two fresh Leader generations produced no first durable progress; hand off to the unique Operator before another generation.",
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
- throw usageError(`Task ${report.taskId} Project ${entry.projectId} base is ${entry.status}; `
117
- + `run 'yui task base status ${report.taskId} --refresh' and choose an explicit delivery base. `
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
- if (report.refreshed) {
129
- throw usageError(`Task ${report.taskId} Project ${entry.projectId} remote base could not be refreshed: `
130
- + `${entry.error ?? "unknown error"}`);
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
- import { createHash } from "node:crypto";
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 generationRefs = unique.map((session) => (`${session.nativeSessionId}@${session.launchId ?? session.createdAt}`));
35
- const progressRefs = progress.map(({ ref }) => ref);
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
- exhausted,
30
+ attentionRecommended,
42
31
  generationsBeforeFirstProgress,
43
32
  ...(firstGenerationAt === undefined ? {} : { firstGenerationAt }),
44
33
  ...(firstProgressAt === undefined ? {} : { firstProgressAt }),
45
- generationRefs,
46
- progressRefs,
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
- : `Fewer than two Leader generations exist before first durable progress.`
38
+ : "Fewer than two Leader generations exist before first durable progress."
53
39
  });
54
40
  }