@zq-silk/yui 0.11.3 → 0.12.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 +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -1,96 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
throw new Error(`Input request ${request.id} recommendation ${policy.recommendedChoiceKey} is missing.`);
|
|
10
|
-
}
|
|
11
|
-
return {
|
|
12
|
-
category: "attention",
|
|
13
|
-
taskId: request.taskId,
|
|
14
|
-
receiptId: formatInputRequestReceiptId(request.taskId, request.id),
|
|
15
|
-
source: { kind: "input-request", taskId: request.taskId, localId: request.id },
|
|
16
|
-
text: [
|
|
17
|
-
"A Task Leader is waiting for a boundary decision. Inspect it before involving the user.",
|
|
18
|
-
"Present it only when it is a user-owned choice, authorization, credential, unavailable external fact, or irreversible operation.",
|
|
19
|
-
`If it is an implementation, scheduling, review, or recoverable runtime choice, do not present it to the user; return it to the Leader with: yui task input cancel ${request.taskId} ${request.id} --reason \"Operator returned: not a user-owned boundary\"`,
|
|
20
|
-
"For a user-owned boundary, do not answer it yourself; do not answer or choose on the user's behalf; relay the user's exact response only.",
|
|
21
|
-
`Task: ${request.taskId}`,
|
|
22
|
-
`Input: ${request.id}`,
|
|
23
|
-
`Question: ${request.question}`,
|
|
24
|
-
...(request.choices.length === 0
|
|
25
|
-
? ["Answer type: free text"]
|
|
26
|
-
: ["Choices:", ...request.choices.map(({ key, label }) => ` ${key}: ${label}`)]),
|
|
27
|
-
...(policy.kind === "required"
|
|
28
|
-
? ["Decision policy: this requires the user's response; there is no automatic fallback."]
|
|
29
|
-
: [
|
|
30
|
-
`Agent recommendation: ${recommendation.key}: ${recommendation.label}`,
|
|
31
|
-
`Automatic fallback after: ${formatTimestamp(policy.timeoutAt, context.timeZone)}`
|
|
32
|
-
]),
|
|
33
|
-
`Inspect: yui task input show ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")}`,
|
|
34
|
-
request.choices.length === 0
|
|
35
|
-
? `After the user replies: yui task input answer ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")} --text "<answer>"`
|
|
36
|
-
: `After the user chooses: yui task input answer ${formatTaskRecordReference(request.taskId, request.id, "inputRequest")} --choice <key>`
|
|
37
|
-
].join("\n")
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
export function createLeaderRecoveryOperatorPresentation(notification) {
|
|
1
|
+
/**
|
|
2
|
+
* One mailbox batch becomes one short synthetic user message. The durable
|
|
3
|
+
* records remain the context authority; this wake text carries only stable
|
|
4
|
+
* identities and exact CLI reads, never copied Task or Provider narrative.
|
|
5
|
+
*/
|
|
6
|
+
export function createOperatorBatchPresentation(batchId, items) {
|
|
7
|
+
if (items.length === 0)
|
|
8
|
+
throw new Error("Operator presentation requires at least one item.");
|
|
41
9
|
return {
|
|
42
|
-
|
|
43
|
-
taskId: notification.taskId,
|
|
44
|
-
receiptId: `leader-recovery:${notification.taskId}:${notification.createdAt}`,
|
|
45
|
-
source: { kind: "leader-recovery", id: notification.taskId },
|
|
10
|
+
receiptId: `operator-batch:${batchId}`,
|
|
46
11
|
text: [
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`Inspect: yui task show ${notification.taskId}`,
|
|
51
|
-
"Recovery status: yui jobs list",
|
|
52
|
-
`Retry after inspection: yui jobs retry leader-recovery:${notification.taskId}`
|
|
12
|
+
"[Yui updates]",
|
|
13
|
+
"Yui recorded the following durable updates. Inspect the referenced records and present only information that changes the user's understanding, authorization, or next action.",
|
|
14
|
+
...items.flatMap(renderItem)
|
|
53
15
|
].join("\n")
|
|
54
16
|
};
|
|
55
17
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`lastProgressAt: ${notification.progressAt}`,
|
|
70
|
-
"Classification: truly-stalled",
|
|
71
|
-
`Evidence: ${notification.evidenceKey}`,
|
|
72
|
-
"Controller preserved the exact Run/Session fence and performed no automatic Enter, reset, retry, kill, or Session replacement.",
|
|
73
|
-
`Inspect: yui task context ${notification.taskId}`,
|
|
74
|
-
"Choose the supported continuation, reset, retry, Agent change, or user-input action only after inspection."
|
|
75
|
-
].join("\n")
|
|
76
|
-
};
|
|
18
|
+
function renderItem(item) {
|
|
19
|
+
if (item.kind === "input-request") {
|
|
20
|
+
const { request } = item;
|
|
21
|
+
return [
|
|
22
|
+
`- User input requested: ${request.taskId}/${request.id}`,
|
|
23
|
+
` Inspect: yui task input show ${request.taskId}/${request.id}`
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
const { event } = item;
|
|
27
|
+
return [
|
|
28
|
+
`- ${eventLabel(event)}: ${event.taskId}/${event.id}`,
|
|
29
|
+
` Inspect: yui task event show ${event.taskId} ${event.id}`
|
|
30
|
+
];
|
|
77
31
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
category: "information",
|
|
87
|
-
taskId: notification.taskId,
|
|
88
|
-
receiptId: `task-terminal:${notification.taskId}:${notification.status}:${notification.createdAt}`,
|
|
89
|
-
source: { kind: "task-terminal", id: notification.taskId },
|
|
90
|
-
text: [
|
|
91
|
-
`Task ${notification.taskId} was ${action} by ${actor}.`,
|
|
92
|
-
`Summary: ${notification.summary}`,
|
|
93
|
-
`Inspect: yui task show ${notification.taskId}`
|
|
94
|
-
].join("\n")
|
|
95
|
-
};
|
|
32
|
+
function eventLabel(event) {
|
|
33
|
+
switch (event.type) {
|
|
34
|
+
case "task.completed": return "Task completed";
|
|
35
|
+
case "task.retired": return "Task retired";
|
|
36
|
+
case "leader.attention-required": return "Task Leader needs attention";
|
|
37
|
+
case "run.stalled": return "Task Leader stalled";
|
|
38
|
+
default: return `Task event ${event.type}`;
|
|
39
|
+
}
|
|
96
40
|
}
|
|
@@ -12,7 +12,8 @@ import { createYieldReceipt } from "../run/yieldReceipt.js";
|
|
|
12
12
|
import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
13
13
|
import { isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
14
14
|
import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
15
|
-
import {
|
|
15
|
+
import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
|
|
16
|
+
import { latestRunDurableProgressAt, RUN_RECOVERY_APPLIED_EVENT, RUN_RECOVERY_REQUESTED_EVENT } from "../scheduler/roleRunStall.js";
|
|
16
17
|
import { markTaskWakeConsumed } from "../scheduler/taskWake.js";
|
|
17
18
|
import { workItemExecutionGroupById, workItemOwnsUnresolvedExecutionLane, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
18
19
|
/**
|
|
@@ -352,7 +353,7 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
352
353
|
/**
|
|
353
354
|
* Leader-controlled recovery boundary for one active AgentRun. This primitive
|
|
354
355
|
* validates every durable fence in one transaction and records only a
|
|
355
|
-
* structured request for retry
|
|
356
|
+
* structured request for same-Run diagnosis/retry. It never writes terminal
|
|
356
357
|
* bytes, retries a provider input, kills a host, or silently rebinds a native
|
|
357
358
|
* generation. Explicit termination is the sole action that changes Run state.
|
|
358
359
|
*/
|
|
@@ -420,6 +421,35 @@ function recoverExactAgentRunInTransaction(store, input) {
|
|
|
420
421
|
if (!matchesRecoverySessionFence(store, input)) {
|
|
421
422
|
return stateChanged("session-or-launch-fence-mismatch");
|
|
422
423
|
}
|
|
424
|
+
const recoveryBlocker = exactRecoveryExecutionBlocker(store, current);
|
|
425
|
+
if (recoveryBlocker !== null) {
|
|
426
|
+
return {
|
|
427
|
+
disposition: "blocked",
|
|
428
|
+
action: input.action,
|
|
429
|
+
run: current,
|
|
430
|
+
progressAt: progress.progressAt,
|
|
431
|
+
reason: recoveryBlocker
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (input.action === "terminate") {
|
|
435
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
436
|
+
const session = sessions?.sessions[input.agentId];
|
|
437
|
+
const providerTurn = sessions?.providerBinding?.turn;
|
|
438
|
+
const exactTerminalEvidence = session?.status === "stopped"
|
|
439
|
+
|| session?.status === "broken"
|
|
440
|
+
|| providerTurn?.status === "failed"
|
|
441
|
+
|| providerTurn?.status === "cancelled"
|
|
442
|
+
|| providerTurn?.status === "rejected";
|
|
443
|
+
if (!exactTerminalEvidence) {
|
|
444
|
+
return {
|
|
445
|
+
disposition: "blocked",
|
|
446
|
+
action: input.action,
|
|
447
|
+
run: current,
|
|
448
|
+
progressAt: progress.progressAt,
|
|
449
|
+
reason: "runtime-not-terminal"
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
423
453
|
if (input.providerAcceptance === "ambiguous"
|
|
424
454
|
&& input.action !== "diagnose") {
|
|
425
455
|
return {
|
|
@@ -496,7 +526,6 @@ function recoverExactAgentRunInTransaction(store, input) {
|
|
|
496
526
|
}
|
|
497
527
|
}
|
|
498
528
|
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, RUN_RECOVERY_APPLIED_EVENT, { ...eventPayload, status: "terminated" }, input.now));
|
|
499
|
-
clearMatchingLeaderStallAttention(store, input.taskId, input.runId);
|
|
500
529
|
if (input.roleName !== "leader") {
|
|
501
530
|
enqueueWork(store, { kind: "role", taskId: input.taskId, roleName: "leader" }, "run-recovery-terminated", input.now, [{ type: "run", taskId: input.taskId, id: input.runId }]);
|
|
502
531
|
}
|
|
@@ -507,6 +536,42 @@ function recoverExactAgentRunInTransaction(store, input) {
|
|
|
507
536
|
progressAt: progress.progressAt
|
|
508
537
|
};
|
|
509
538
|
}
|
|
539
|
+
function exactRecoveryExecutionBlocker(store, run) {
|
|
540
|
+
const sessions = store.getTaskRoleSessionSet(run.taskId, run.roleName);
|
|
541
|
+
const binding = sessions?.providerBinding;
|
|
542
|
+
const mailbox = store.getWorkMailbox({
|
|
543
|
+
kind: "role",
|
|
544
|
+
taskId: run.taskId,
|
|
545
|
+
roleName: run.roleName
|
|
546
|
+
});
|
|
547
|
+
if (mailbox?.inputDelivery != null)
|
|
548
|
+
return "provider-input-delivery-unsettled";
|
|
549
|
+
if (runOwnsBlockingProviderContinuation(store.listEvents(run.taskId), {
|
|
550
|
+
taskId: run.taskId,
|
|
551
|
+
roleName: run.roleName,
|
|
552
|
+
runId: run.id,
|
|
553
|
+
agentId: run.effective.agentId
|
|
554
|
+
}))
|
|
555
|
+
return "provider-continuation-writer-owned";
|
|
556
|
+
if (runHasActiveRuntimeOperations(store.listEvents(run.taskId), {
|
|
557
|
+
taskId: run.taskId,
|
|
558
|
+
roleName: run.roleName,
|
|
559
|
+
runId: run.id,
|
|
560
|
+
agentId: run.effective.agentId
|
|
561
|
+
}))
|
|
562
|
+
return "provider-operation-active";
|
|
563
|
+
if (run.deliveredAt !== undefined
|
|
564
|
+
&& (binding === null || binding?.turn === null))
|
|
565
|
+
return "provider-turn-state-missing";
|
|
566
|
+
if (binding === null || binding === undefined)
|
|
567
|
+
return null;
|
|
568
|
+
if (["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn?.status ?? ""))
|
|
569
|
+
return "provider-turn-unsettled";
|
|
570
|
+
if (binding.authority.owner === "human" || binding.authority.owner === "unknown") {
|
|
571
|
+
return "provider-writer-authority-unavailable";
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
510
575
|
function matchesRecoverySessionFence(store, input) {
|
|
511
576
|
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
512
577
|
// Recovery must never proceed without a durable Session fence. A missing
|
|
@@ -521,8 +586,7 @@ function matchesRecoverySessionFence(store, input) {
|
|
|
521
586
|
return false;
|
|
522
587
|
if (session.agentId !== input.agentId || session.adapterId !== input.adapterId)
|
|
523
588
|
return false;
|
|
524
|
-
//
|
|
525
|
-
// needed. Preserve its exact identity as the CAS fence; only same-Session
|
|
589
|
+
// Preserve a dead Session's exact identity as the CAS fence; only same-Session
|
|
526
590
|
// retry is invalid once the native process is stopped or broken.
|
|
527
591
|
if ((session.status === "stopped" || session.status === "broken")
|
|
528
592
|
&& input.action === "retry")
|
|
@@ -363,6 +363,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
363
363
|
let stopped = 0;
|
|
364
364
|
let other = 0;
|
|
365
365
|
let resets = 0;
|
|
366
|
+
let conversationSwitches = 0;
|
|
366
367
|
let lifecycleEvents = 0;
|
|
367
368
|
let stopFailures = 0;
|
|
368
369
|
const terminalByRunRelation = {
|
|
@@ -393,6 +394,9 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
393
394
|
continue;
|
|
394
395
|
if (event.type === "runtime.role-session-reset")
|
|
395
396
|
resets += 1;
|
|
397
|
+
else if (event.type === "runtime.conversation-switch-resolved"
|
|
398
|
+
&& event.payload.status === "applied")
|
|
399
|
+
conversationSwitches += 1;
|
|
396
400
|
else if (runtimeObservationFromTaskEvent(event)?.kind.startsWith("session.")) {
|
|
397
401
|
lifecycleEvents += 1;
|
|
398
402
|
}
|
|
@@ -406,6 +410,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
406
410
|
stopped,
|
|
407
411
|
other,
|
|
408
412
|
resets,
|
|
413
|
+
conversationSwitches,
|
|
409
414
|
lifecycleEvents,
|
|
410
415
|
stopFailures,
|
|
411
416
|
terminalByRunRelation
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
2
3
|
import { projectFirstProgressAdvisory } from "../runtime/firstProgressAdvisory.js";
|
|
3
4
|
/** One Task's orchestration cost and advisory projection, with no writes. */
|
|
@@ -129,10 +130,14 @@ function projectAdvisories(facts, classifications, fullRounds, repeatedIdentitie
|
|
|
129
130
|
const recent = semanticFull.slice(-3);
|
|
130
131
|
const recentIds = new Set(recent.map(({ id }) => id));
|
|
131
132
|
const newFinding = facts.reviewFindings.some(({ firstReviewRoundId }) => recentIds.has(firstReviewRoundId));
|
|
132
|
-
|
|
133
|
+
const sameCandidate = recent.length === 3
|
|
134
|
+
&& recent.every((round) => isDeepStrictEqual(round.taskCandidate, recent[0].taskCandidate));
|
|
135
|
+
const sameReviewer = recent.length === 3
|
|
136
|
+
&& recent.every((round) => round.reviewerRoleName === recent[0].reviewerRoleName);
|
|
137
|
+
if (sameCandidate && sameReviewer && !newFinding) {
|
|
133
138
|
result.push({
|
|
134
|
-
code: "
|
|
135
|
-
reason:
|
|
139
|
+
code: "repeated-full-review",
|
|
140
|
+
reason: `The same Reviewer completed three full semantic Reviews of the same frozen candidate without a new finding; this is a cost advisory, not a Review limit.`,
|
|
136
141
|
refs: recent.map(({ id }) => `review-round:${id}`)
|
|
137
142
|
});
|
|
138
143
|
}
|
|
@@ -75,7 +75,7 @@ export function builtinAgentProfileInputs() {
|
|
|
75
75
|
{
|
|
76
76
|
id: "reviewer",
|
|
77
77
|
description: "Review one candidate against the user's core outcome, supported behavior, and direct evidence.",
|
|
78
|
-
instructions: "Start from user intent and acceptance criteria. Inspect the complete relevant change and report only reachable, material, actionable problems with direct evidence. Separate defects from verification gaps, and prefer the smallest sufficient correction. Follow the bound Project's Policy and Knowledge for build, test, migration, release, and review expectations; do not import rules from another Project or Task. For normal software delivery, review the frozen Task result as one final ReviewRound rather than inventing a per-WorkItem protocol unless the Project Policy explicitly requires one. A Task-final Round has no synthetic WorkItem anchor, and
|
|
78
|
+
instructions: "Start from user intent and acceptance criteria. Inspect the complete relevant change and report only reachable, material, actionable problems with direct evidence. Separate defects from verification gaps, and prefer the smallest sufficient correction. Follow the bound Project's Policy and Knowledge for build, test, migration, release, and review expectations; do not import rules from another Project or Task. For normal software delivery, review the frozen Task result as one final ReviewRound rather than inventing a per-WorkItem protocol unless the Project Policy explicitly requires one. A Task-final Round has no synthetic WorkItem anchor, and the Reviewer Session and physical workspace continue across changed-head Rounds without reusing an earlier verdict. The current Run's frozen candidate remains the only scope even if Task main advances. For Delta Recheck, return equivalent-and-accepted, finding, or requires-full-review with explicit reasoning; never create the next Round. Do not turn speculative or extreme edge cases into new state, retries, fallbacks, or protocol. In the Review workspace you may edit source or tests, run local checks, and optionally commit diagnostic evidence. Never push, integrate, mutate Task state, touch another workspace or stable checkout, or write the real Yui control-plane home. Report complete findings, checks actually run, uncertainty, and bounded next actions through the exact Review yield; Yui preserves the full free-form report. Expose evidence and options to the Leader, who decides.",
|
|
79
79
|
defaultAccess: "write"
|
|
80
80
|
}
|
|
81
81
|
];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { UNSUPPORTED, collectRuntimeBuildIdentity, createProductionRuntimeIdentityPorts } from "../observability/runtimeIdentity.js";
|
|
4
|
+
import { readCompatibleHomeIdentity } from "../storage/compatibleTaskStore.js";
|
|
5
|
+
import { detectRunningRelease, readActiveReleasePointer, releaseDirectoryName } from "./runtimeRelease.js";
|
|
6
|
+
/**
|
|
7
|
+
* Prevent a checkout or otherwise unverified CLI from entering a Home that is
|
|
8
|
+
* already owned by an immutable active release. This fence runs before any
|
|
9
|
+
* TaskStore is opened, so even schema discovery cannot advance SQLite state.
|
|
10
|
+
* Published global packages remain the stable ordinary CLI boundary; an exact
|
|
11
|
+
* installed release may also run when it is active or is the explicit handover
|
|
12
|
+
* target.
|
|
13
|
+
*/
|
|
14
|
+
export function assertCliHomeReleaseFence(input) {
|
|
15
|
+
const active = readActiveReleasePointer(input.home);
|
|
16
|
+
if (active === null)
|
|
17
|
+
return "unregistered-home";
|
|
18
|
+
const running = detectRunningRelease(input.entryPath);
|
|
19
|
+
if (running !== null) {
|
|
20
|
+
if (matchesActiveRelease(running.manifest, active))
|
|
21
|
+
return "active-release";
|
|
22
|
+
if (isExplicitActivation(input.args, running.manifest))
|
|
23
|
+
return "release-activation";
|
|
24
|
+
throw fenceError(input, active, "installed-release", running.manifest);
|
|
25
|
+
}
|
|
26
|
+
const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(input.packageRoot, input.entryPath));
|
|
27
|
+
const distribution = readPackageDistribution(input.packageRoot);
|
|
28
|
+
if (distribution === "published" && build.sourceCommit === UNSUPPORTED) {
|
|
29
|
+
return "published-package";
|
|
30
|
+
}
|
|
31
|
+
const source = distribution === "checkout" || build.sourceCommit !== UNSUPPORTED
|
|
32
|
+
? "checkout-local"
|
|
33
|
+
: "unverified-local";
|
|
34
|
+
throw fenceError(input, active, source, undefined, build);
|
|
35
|
+
}
|
|
36
|
+
/** Render bounded, non-secret identity evidence for a migration diagnosis. */
|
|
37
|
+
export function describeCliHomeInvocation(input) {
|
|
38
|
+
const running = detectRunningRelease(input.entryPath);
|
|
39
|
+
const collected = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(input.packageRoot, input.entryPath));
|
|
40
|
+
const distribution = readPackageDistribution(input.packageRoot);
|
|
41
|
+
const source = running !== null
|
|
42
|
+
? "installed-release"
|
|
43
|
+
: distribution === "published" && collected.sourceCommit === UNSUPPORTED
|
|
44
|
+
? "published-package"
|
|
45
|
+
: distribution === "checkout" || collected.sourceCommit !== UNSUPPORTED
|
|
46
|
+
? "checkout-local"
|
|
47
|
+
: "unverified-local";
|
|
48
|
+
let homeId = "unknown";
|
|
49
|
+
try {
|
|
50
|
+
homeId = readCompatibleHomeIdentity(input.home).homeId;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// The primary migration error remains authoritative when identity is unreadable.
|
|
54
|
+
}
|
|
55
|
+
let activeRelease = "none";
|
|
56
|
+
let activeBuild = "none";
|
|
57
|
+
try {
|
|
58
|
+
const active = readActiveReleasePointer(input.home);
|
|
59
|
+
if (active !== null) {
|
|
60
|
+
activeRelease = active.releaseId;
|
|
61
|
+
activeBuild = active.buildId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
activeRelease = "unreadable";
|
|
66
|
+
activeBuild = "unreadable";
|
|
67
|
+
}
|
|
68
|
+
const version = running?.manifest.version ?? collected.packageVersion;
|
|
69
|
+
const buildId = running?.manifest.buildId
|
|
70
|
+
?? (source === "checkout-local" ? "checkout" : source);
|
|
71
|
+
const sourceCommit = running?.manifest.sourceCommit ?? (collected.sourceCommit === UNSUPPORTED ? "unknown" : collected.sourceCommit);
|
|
72
|
+
return [
|
|
73
|
+
`CLI: version=${diagnostic(version)} build=${diagnostic(buildId)} sourceCommit=${diagnostic(sourceCommit)}`,
|
|
74
|
+
`Home: homeId=${diagnostic(homeId)} activeRelease=${diagnostic(activeRelease)} activeBuild=${diagnostic(activeBuild)}`,
|
|
75
|
+
`Invocation: source=${source}`
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
function matchesActiveRelease(manifest, active) {
|
|
79
|
+
return active.releaseId === releaseDirectoryName(manifest)
|
|
80
|
+
&& active.version === manifest.version
|
|
81
|
+
&& active.buildId === manifest.buildId
|
|
82
|
+
&& active.packageDigest === manifest.packageDigest;
|
|
83
|
+
}
|
|
84
|
+
function isExplicitActivation(args, manifest) {
|
|
85
|
+
if (args.length !== 3 || args[0] !== "release" || args[1] !== "activate")
|
|
86
|
+
return false;
|
|
87
|
+
return args[2] === releaseDirectoryName(manifest) || args[2] === manifest.buildId;
|
|
88
|
+
}
|
|
89
|
+
function readPackageDistribution(packageRoot) {
|
|
90
|
+
try {
|
|
91
|
+
const value = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
92
|
+
if (value.private === true)
|
|
93
|
+
return "checkout";
|
|
94
|
+
if (value.private === false)
|
|
95
|
+
return "published";
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// A missing or damaged package identity is not publication evidence.
|
|
99
|
+
}
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
function fenceError(input, active, source, release, collected = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(input.packageRoot, input.entryPath))) {
|
|
103
|
+
let homeId = "unknown";
|
|
104
|
+
try {
|
|
105
|
+
homeId = readCompatibleHomeIdentity(input.home).homeId;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// The release fence must still diagnose and reject an unreadable Home.
|
|
109
|
+
}
|
|
110
|
+
const version = release?.version ?? collected.packageVersion;
|
|
111
|
+
const buildId = release?.buildId ?? (source === "checkout-local" ? "checkout" : "unknown");
|
|
112
|
+
const sourceCommit = release?.sourceCommit ?? (collected.sourceCommit === UNSUPPORTED ? "unknown" : collected.sourceCommit);
|
|
113
|
+
return new Error([
|
|
114
|
+
`Refusing ${source} CLI access to a Home owned by another active release.`,
|
|
115
|
+
`CLI: version=${diagnostic(version)} build=${diagnostic(buildId)} sourceCommit=${diagnostic(sourceCommit)}`,
|
|
116
|
+
`Home: homeId=${diagnostic(homeId)} activeRelease=${diagnostic(active.releaseId)} activeBuild=${diagnostic(active.buildId)}`,
|
|
117
|
+
`Invocation: source=${source}`,
|
|
118
|
+
"Use the published global yui for this Home, or run make install-local without overriding its isolated YUI_HOME."
|
|
119
|
+
].join("\n"));
|
|
120
|
+
}
|
|
121
|
+
function diagnostic(value) {
|
|
122
|
+
return value.replace(/[^A-Za-z0-9._:+/@-]/gu, "?").slice(0, 160);
|
|
123
|
+
}
|
|
@@ -267,6 +267,26 @@ export function isHandoverLockHeld(home) {
|
|
|
267
267
|
return !isEnoent(error);
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* True when a live handover is owned by a process other than `allowedOwnerPid`.
|
|
272
|
+
*
|
|
273
|
+
* Foreground maintenance commands acquire the lock and still need to call the
|
|
274
|
+
* Controller they are draining, while every unrelated CLI/managed Session must
|
|
275
|
+
* wait rather than starting a second Controller during the replacement window.
|
|
276
|
+
* An unreadable lock remains a foreign live lock so callers fail closed.
|
|
277
|
+
*/
|
|
278
|
+
export function isForeignHandoverLockHeld(home, allowedOwnerPid = process.pid) {
|
|
279
|
+
const lockPath = join(resolve(home), "runtime", "handover.lock");
|
|
280
|
+
try {
|
|
281
|
+
const owner = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
282
|
+
if (!isHandoverLockLive(owner))
|
|
283
|
+
return false;
|
|
284
|
+
return owner.pid !== allowedOwnerPid;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
return !isEnoent(error);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
270
290
|
export function acquireHandoverLock(home) {
|
|
271
291
|
const lockPath = join(resolve(home), "runtime", "handover.lock");
|
|
272
292
|
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|