@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
|
@@ -281,11 +281,14 @@ export class TmuxSessionHost {
|
|
|
281
281
|
return binding;
|
|
282
282
|
}
|
|
283
283
|
const broker = launchBrokerForHome(yuiHome);
|
|
284
|
+
const sessionManifest = planned.launch.env.YUI_SESSION_MANIFEST;
|
|
284
285
|
const frozenControlPlane = planned.launch.env[YUI_CONTROL_PLANE_DESCRIPTOR];
|
|
285
286
|
const frozenTaskRuntime = planned.launch.env[YUI_TASK_RUNTIME_DESCRIPTOR];
|
|
286
287
|
if (request.owner.scope === "task" && request.runId !== undefined
|
|
287
|
-
&& (
|
|
288
|
-
|
|
288
|
+
&& (sessionManifest === undefined
|
|
289
|
+
|| frozenControlPlane === undefined
|
|
290
|
+
|| frozenTaskRuntime === undefined)) {
|
|
291
|
+
throw new Error("Managed Task Agent Host launch is missing its Session Manifest or frozen control descriptors.");
|
|
289
292
|
}
|
|
290
293
|
const reservation = broker.reserve(Object.freeze({
|
|
291
294
|
schemaVersion: 1,
|
|
@@ -324,6 +327,9 @@ export class TmuxSessionHost {
|
|
|
324
327
|
...(planned.launch.env.YUI_WORKSPACE === undefined
|
|
325
328
|
? {}
|
|
326
329
|
: { YUI_WORKSPACE: planned.launch.env.YUI_WORKSPACE }),
|
|
330
|
+
...(sessionManifest === undefined
|
|
331
|
+
? {}
|
|
332
|
+
: { YUI_SESSION_MANIFEST: sessionManifest }),
|
|
327
333
|
...(frozenControlPlane === undefined
|
|
328
334
|
? {}
|
|
329
335
|
: { [YUI_CONTROL_PLANE_DESCRIPTOR]: frozenControlPlane }),
|
|
@@ -9,7 +9,6 @@ import { recordLeaderFailure } from "./leaderFailure.js";
|
|
|
9
9
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
10
10
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
11
11
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
12
|
-
import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
13
12
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
14
13
|
const results = [];
|
|
15
14
|
const wakeups = selection === undefined || selection.full
|
|
@@ -128,30 +127,6 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
128
127
|
&& existingSession.status !== "stopped"
|
|
129
128
|
&& existingSession.status !== "broken";
|
|
130
129
|
const mode = resumableSession && compatibleSession ? "resume" : "new";
|
|
131
|
-
if (mode === "new" && store.getTaskRoleSessionSet !== undefined) {
|
|
132
|
-
const stopLoss = projectFirstProgressStopLoss({
|
|
133
|
-
sessions: store.getTaskRoleSessionSet(task.id, role.name),
|
|
134
|
-
events: store.listEvents?.(task.id) ?? [],
|
|
135
|
-
workItems: store.listWorkItems?.(task.id) ?? [],
|
|
136
|
-
reviewRounds: store.listReviewRounds?.(task.id) ?? [],
|
|
137
|
-
integrations: store.listIntegrationAttempts?.(task.id) ?? []
|
|
138
|
-
});
|
|
139
|
-
if (stopLoss.exhausted && store.saveLeaderFirstProgressStopLoss !== undefined) {
|
|
140
|
-
const saved = store.saveLeaderFirstProgressStopLoss({
|
|
141
|
-
taskId: task.id,
|
|
142
|
-
roleName: role.name,
|
|
143
|
-
expectedFingerprint: stopLoss.fingerprint,
|
|
144
|
-
now
|
|
145
|
-
});
|
|
146
|
-
results.push({
|
|
147
|
-
taskId: task.id,
|
|
148
|
-
status: "skipped",
|
|
149
|
-
reason: saved === "recorded" ? "recovery-blocked" : "state-changed",
|
|
150
|
-
error: saved === "recorded" ? stopLoss.reason : undefined
|
|
151
|
-
});
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
130
|
const runId = store.peekNextAgentRunId(task.id);
|
|
156
131
|
const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
|
|
157
132
|
const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
|
|
@@ -176,11 +176,6 @@ function saveMinimumConfiguration(store, agent, workspace, usableAgentIds) {
|
|
|
176
176
|
scope: "global",
|
|
177
177
|
roleName: SYSTEM_OPERATOR_ROLE
|
|
178
178
|
}, "desired Agent binding update");
|
|
179
|
-
const duplicateAdapter = Object.values(operator.agentBindings).find((candidate) => candidate.adapterId === agent.adapterId && candidate.agentId !== agent.id);
|
|
180
|
-
if (duplicateAdapter !== undefined) {
|
|
181
|
-
throw usageError(`Operator already has a ${agent.adapterId} Agent: ${duplicateAdapter.agentId}. `
|
|
182
|
-
+ "Use yui config agent update or yui config role bind to repair the Operator configuration.");
|
|
183
|
-
}
|
|
184
179
|
tx.saveGlobalRole(updateGlobalRole(operator, {
|
|
185
180
|
activeAgentId: agent.id,
|
|
186
181
|
agentBindings: { ...operator.agentBindings, [agent.id]: binding }
|
|
@@ -727,6 +727,7 @@ export class SqliteTaskStore {
|
|
|
727
727
|
workItems: this.#sortById(this.#listPayload("work_items", "task_id = ?", [taskId]), (item) => item.id),
|
|
728
728
|
changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
|
|
729
729
|
integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
|
|
730
|
+
integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
|
|
730
731
|
reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
|
|
731
732
|
taskFinalReviewContractEvents: events
|
|
732
733
|
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
@@ -422,6 +422,7 @@ export class FileTaskStore {
|
|
|
422
422
|
workItems: values(aggregate.workItems, "id"),
|
|
423
423
|
changeSets: values(aggregate.changeSets, "id"),
|
|
424
424
|
integrations: values(aggregate.integrationAttempts, "id"),
|
|
425
|
+
integrationQueueEntries: values(aggregate.integrationQueue, "id"),
|
|
425
426
|
reviewRounds: values(aggregate.reviewRounds, "id"),
|
|
426
427
|
taskFinalReviewContractEvents: events
|
|
427
428
|
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
@@ -298,7 +298,7 @@ function describeOfflineBlockers(inventory) {
|
|
|
298
298
|
return `${index + 1}. ${identity.length === 0 ? "identity=unknown" : identity} ` +
|
|
299
299
|
`reason=${reason}`;
|
|
300
300
|
});
|
|
301
|
-
return `Offline migration
|
|
301
|
+
return `Offline Home migration cannot start while ${inventory.total} runtime obligation(s) remain. ` +
|
|
302
302
|
lines.join("; ");
|
|
303
303
|
}
|
|
304
304
|
async function readOfflineInventory(options, home) {
|
|
@@ -315,15 +315,43 @@ async function readOfflineInventory(options, home) {
|
|
|
315
315
|
}
|
|
316
316
|
function offlineInventoryBlocker(inventory, sceneUnchanged) {
|
|
317
317
|
const lifecycleOnly = inventory.blockers.every(({ reason }) => (reason === "pending-mailbox" || reason === "pending-inbox"));
|
|
318
|
+
const runtimeUnknown = inventory.blockers.some(({ reason }) => (reason === "native-session-unknown"));
|
|
319
|
+
const action = lifecycleOnly
|
|
320
|
+
? (sceneUnchanged
|
|
321
|
+
? "No binary, Controller, fence, or Home change was made. Wait for the listed lifecycle "
|
|
322
|
+
+ "events to settle, then re-run `yui update`."
|
|
323
|
+
: "The Home was not switched. Wait for the listed lifecycle events to settle, then "
|
|
324
|
+
+ "re-run `yui update`.")
|
|
325
|
+
: runtimeUnknown
|
|
326
|
+
? (sceneUnchanged
|
|
327
|
+
? "Offline Home migration could not confirm that every native Session is stopped. Run "
|
|
328
|
+
+ "`yui controller status --verbose` from a normal shell and inspect the reported "
|
|
329
|
+
+ "runtime ownership. Use `yui controller cleanup` only for resources it classifies "
|
|
330
|
+
+ "for cleanup, then re-run `yui update`; do not kill unknown processes blindly. No "
|
|
331
|
+
+ "binary, Controller, fence, or Home change was made."
|
|
332
|
+
: "The Home was not switched because native Session state could not be confirmed. Run "
|
|
333
|
+
+ "`yui controller status --verbose`, inspect the reported ownership, and use "
|
|
334
|
+
+ "`yui controller cleanup` only for resources it classifies for cleanup before "
|
|
335
|
+
+ "retrying; do not kill unknown processes blindly.")
|
|
336
|
+
: (sceneUnchanged
|
|
337
|
+
? "This version requires an offline Home migration with every managed Agent Session "
|
|
338
|
+
+ "stopped. Let the listed Turns or Runs finish. If the installed `yui` supports "
|
|
339
|
+
+ "`session stop --all`, run `yui session stop --all` from a normal shell; otherwise "
|
|
340
|
+
+ "exit every listed managed Session manually. Then re-run `yui update`. No binary, "
|
|
341
|
+
+ "Controller, fence, or Home change was made."
|
|
342
|
+
: "The Home was not switched. Let the listed Turns or Runs finish. If the installed "
|
|
343
|
+
+ "`yui` supports `session stop --all`, run `yui session stop --all` from a normal "
|
|
344
|
+
+ "shell; otherwise exit every listed managed Session manually. Then re-run "
|
|
345
|
+
+ "`yui update`.");
|
|
318
346
|
return {
|
|
319
347
|
outcome: "blocked",
|
|
320
|
-
stage: lifecycleOnly
|
|
348
|
+
stage: lifecycleOnly
|
|
349
|
+
? "drain-incomplete"
|
|
350
|
+
: runtimeUnknown
|
|
351
|
+
? "runtime-unknown"
|
|
352
|
+
: "active-sessions",
|
|
321
353
|
message: describeOfflineBlockers(inventory),
|
|
322
|
-
action
|
|
323
|
-
? "No binary, Controller, fence, or Home change was made. Keep working; when every " +
|
|
324
|
-
"listed runtime obligation is clear, re-run `yui update` so preflight is repeated."
|
|
325
|
-
: "The Home was not switched. Let every listed runtime obligation settle, then re-run " +
|
|
326
|
-
"`yui update`; do not kill, reset, rebind, or retry the blocked runtime blindly.",
|
|
354
|
+
action,
|
|
327
355
|
blockers: inventory.blockers,
|
|
328
356
|
retryCommand: "yui update",
|
|
329
357
|
...(sceneUnchanged ? { sceneUnchanged: true } : {})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { changeSetDeliverySettled, governingChangeSets, integrationAttemptRequiresSettlement, latestGoverningQueueEntries } from "../integration/deliveryObligation.js";
|
|
1
2
|
import { isReviewFindingBlocking } from "../review/reviewFinding.js";
|
|
2
3
|
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
4
|
import { isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
4
5
|
import { reviewFindingLedgerWriteFailedFromEvents } from "../review/reviewFindingLedger.js";
|
|
6
|
+
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
5
7
|
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
6
8
|
const ACTIVE_JOB_STATUSES = new Set([
|
|
7
9
|
"queued",
|
|
@@ -20,6 +22,7 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
20
22
|
const advisories = [];
|
|
21
23
|
const { task } = facts;
|
|
22
24
|
const findingsGate = options.findingsGate ?? true;
|
|
25
|
+
const taskFinalReviewRequired = resolveRecordedTaskFinalReviewContract(task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents) !== undefined;
|
|
23
26
|
// A pending/running Task-final Review must be resumed or blocked first.
|
|
24
27
|
for (const round of facts.reviewRounds) {
|
|
25
28
|
if ((round.scope ?? "work-item") !== "task")
|
|
@@ -49,7 +52,8 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
49
52
|
.sort((left, right) => (left.createdAt.localeCompare(right.createdAt)
|
|
50
53
|
|| left.id.localeCompare(right.id, undefined, { numeric: true })))
|
|
51
54
|
.at(-1);
|
|
52
|
-
if (
|
|
55
|
+
if (taskFinalReviewRequired
|
|
56
|
+
&& latestCompletedTaskReview !== undefined
|
|
53
57
|
&& deltaRecheckBlocksAcceptance(latestCompletedTaskReview)) {
|
|
54
58
|
const round = latestCompletedTaskReview;
|
|
55
59
|
const disposition = round.deltaRecheck.disposition;
|
|
@@ -97,9 +101,10 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
97
101
|
fix: `wait for DurableJob ${job.id} to finish, or cancel it`
|
|
98
102
|
});
|
|
99
103
|
}
|
|
100
|
-
// Integration obligations
|
|
101
|
-
//
|
|
102
|
-
//
|
|
104
|
+
// Integration obligations follow only the Candidate that currently governs
|
|
105
|
+
// each independent delivery unit. Superseded Candidate history remains
|
|
106
|
+
// visible without keeping the Task open forever.
|
|
107
|
+
const deliveryChangeSets = governingChangeSets(facts.workItems, facts.changeSets);
|
|
103
108
|
for (const item of facts.workItems) {
|
|
104
109
|
if (item.status !== "completed")
|
|
105
110
|
continue;
|
|
@@ -107,7 +112,7 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
107
112
|
const hasGitDelivery = candidate?.workspace !== undefined
|
|
108
113
|
|| candidate?.gitSnapshot !== undefined;
|
|
109
114
|
if (hasGitDelivery
|
|
110
|
-
&& !
|
|
115
|
+
&& !deliveryChangeSets.some((changeSet) => changeSet.workItemId === item.id)) {
|
|
111
116
|
blockers.push({
|
|
112
117
|
code: "integration-evidence-missing",
|
|
113
118
|
ref: ref("work-item", item.id),
|
|
@@ -116,8 +121,8 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
116
121
|
});
|
|
117
122
|
}
|
|
118
123
|
}
|
|
119
|
-
for (const changeSet of
|
|
120
|
-
if (facts.integrations
|
|
124
|
+
for (const changeSet of deliveryChangeSets) {
|
|
125
|
+
if (changeSetDeliverySettled(changeSet, facts.integrations, facts.integrationQueueEntries))
|
|
121
126
|
continue;
|
|
122
127
|
blockers.push({
|
|
123
128
|
code: "integration-evidence-missing",
|
|
@@ -126,10 +131,13 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
126
131
|
fix: `yui task integration start ${task.id} --project ${changeSet.projectId} --change-set ${changeSet.id}`
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
|
-
//
|
|
134
|
+
// Current delivery Attempts and any Attempt that may still be writing must
|
|
135
|
+
// settle. Historical blocked Attempts remain audit evidence only.
|
|
130
136
|
for (const integration of facts.integrations) {
|
|
131
137
|
if (!UNRESOLVED_INTEGRATION_STATUSES.has(integration.status))
|
|
132
138
|
continue;
|
|
139
|
+
if (!integrationAttemptRequiresSettlement(integration, deliveryChangeSets))
|
|
140
|
+
continue;
|
|
133
141
|
blockers.push({
|
|
134
142
|
code: "unresolved-integration",
|
|
135
143
|
ref: ref("integration-attempt", integration.id),
|
|
@@ -138,7 +146,7 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
138
146
|
});
|
|
139
147
|
}
|
|
140
148
|
// Unsettled integration queue entries must commit or be superseded.
|
|
141
|
-
for (const entry of facts.integrationQueueEntries) {
|
|
149
|
+
for (const entry of latestGoverningQueueEntries(deliveryChangeSets, facts.integrationQueueEntries)) {
|
|
142
150
|
if (entry.status === "committed" || entry.status === "superseded")
|
|
143
151
|
continue;
|
|
144
152
|
blockers.push({
|
package/dist/task/nextAction.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { changeSetDeliverySettled, governingChangeSets } from "../integration/deliveryObligation.js";
|
|
2
3
|
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
4
|
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
4
5
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
@@ -253,7 +254,10 @@ export function projectNextAction(facts) {
|
|
|
253
254
|
judgmentRequired: "Leader must choose the execution path: direct execution, a native subagent, or managed Task Role dispatch."
|
|
254
255
|
});
|
|
255
256
|
}
|
|
256
|
-
if (facts.workItems.length === 0
|
|
257
|
+
if (facts.workItems.length === 0
|
|
258
|
+
&& !taskFinalReviewRequired(facts)
|
|
259
|
+
&& !facts.reviewRounds.some((round) => ((round.scope ?? "work-item") === "task"
|
|
260
|
+
&& (round.status === "pending" || round.status === "running")))) {
|
|
257
261
|
const reviewAlternative = facts.reviewConfig === null
|
|
258
262
|
? []
|
|
259
263
|
: [{
|
|
@@ -296,8 +300,8 @@ export function projectNextAction(facts) {
|
|
|
296
300
|
recommendedCommand: `yui task work capture ${task.id}/${uncaptured.id}`
|
|
297
301
|
});
|
|
298
302
|
}
|
|
299
|
-
const unintegrated = facts.changeSets
|
|
300
|
-
.find((changeSet) => !
|
|
303
|
+
const unintegrated = governingChangeSets(facts.workItems, facts.changeSets)
|
|
304
|
+
.find((changeSet) => !changeSetDeliverySettled(changeSet, facts.integrations, facts.integrationQueueEntries));
|
|
301
305
|
if (unintegrated !== undefined) {
|
|
302
306
|
return buildAction(facts, {
|
|
303
307
|
kind: "integrate-change-set",
|
|
@@ -310,11 +314,13 @@ export function projectNextAction(facts) {
|
|
|
310
314
|
recommendedCommand: `yui task integration start ${task.id} --project ${unintegrated.projectId} --change-set ${unintegrated.id}`
|
|
311
315
|
});
|
|
312
316
|
}
|
|
317
|
+
const finalReviewRequired = taskFinalReviewRequired(facts);
|
|
313
318
|
const failedFinal = latestTaskFinalReview(facts.reviewRounds);
|
|
314
319
|
const failedFinalOutcome = failedFinal === undefined
|
|
315
320
|
? null
|
|
316
321
|
: classifyReviewRoundOutcome(failedFinal, nextActionReviewOutcomeEvidence(facts));
|
|
317
|
-
if (
|
|
322
|
+
if (finalReviewRequired
|
|
323
|
+
&& failedFinal !== undefined && failedFinalOutcome?.kind === "non-semantic") {
|
|
318
324
|
return buildAction(facts, {
|
|
319
325
|
kind: "resume-review",
|
|
320
326
|
reason: `Task-final Review ${failedFinal.id} ended before a semantic review was proven.`,
|
|
@@ -325,7 +331,8 @@ export function projectNextAction(facts) {
|
|
|
325
331
|
recommendedCommand: `yui task review force-fresh ${task.id}/${failedFinal.id}`
|
|
326
332
|
});
|
|
327
333
|
}
|
|
328
|
-
if (
|
|
334
|
+
if (finalReviewRequired
|
|
335
|
+
&& failedFinal !== undefined && failedFinalOutcome?.kind === "ambiguous") {
|
|
329
336
|
return buildAction(facts, {
|
|
330
337
|
kind: "repair-protocol-inconsistency",
|
|
331
338
|
reason: `Task-final Review ${failedFinal.id} has ambiguous semantic and infrastructure evidence: ${failedFinalOutcome.reason}`,
|
|
@@ -336,7 +343,8 @@ export function projectNextAction(facts) {
|
|
|
336
343
|
]
|
|
337
344
|
});
|
|
338
345
|
}
|
|
339
|
-
if (
|
|
346
|
+
if (finalReviewRequired
|
|
347
|
+
&& failedFinal !== undefined
|
|
340
348
|
&& failedFinalOutcome?.kind === "semantic"
|
|
341
349
|
&& ((failedFinal.checks ?? []).some(({ outcome }) => outcome === "failed")
|
|
342
350
|
|| deltaRecheckBlocksAcceptance(failedFinal))) {
|
|
@@ -415,7 +423,6 @@ export function projectNextAction(facts) {
|
|
|
415
423
|
recommendedCommand: `yui task review retry ${task.id}/${activeFinal.id}`
|
|
416
424
|
});
|
|
417
425
|
}
|
|
418
|
-
const finalReviewRequired = taskFinalReviewRequired(facts);
|
|
419
426
|
if (task.projectBindings.length > 0
|
|
420
427
|
&& finalReviewRequired
|
|
421
428
|
&& !hasValidFinalReview(facts)) {
|
|
@@ -430,7 +437,7 @@ export function projectNextAction(facts) {
|
|
|
430
437
|
? [{ fact: "Valid established Task-final Review at the direct head", satisfied: false }]
|
|
431
438
|
: [
|
|
432
439
|
{ fact: "All Work Items are terminal", satisfied: true },
|
|
433
|
-
{ fact: "Every ChangeSet is
|
|
440
|
+
{ fact: "Every governing ChangeSet is settled", satisfied: true },
|
|
434
441
|
{ fact: "Valid Task-final Review at the integrated head", satisfied: false }
|
|
435
442
|
],
|
|
436
443
|
recommendedCommand: facts.workItems.length === 0
|
|
@@ -461,11 +468,13 @@ export function projectNextAction(facts) {
|
|
|
461
468
|
: facts.workItems.length === 0
|
|
462
469
|
? [{ fact: "Task main is clean, committed, and verified", satisfied: false }]
|
|
463
470
|
: [
|
|
464
|
-
{ fact: "Every ChangeSet is
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
471
|
+
{ fact: "Every governing ChangeSet is settled", satisfied: true },
|
|
472
|
+
...(finalReviewRequired
|
|
473
|
+
? [{
|
|
474
|
+
fact: "Valid Task-final Review at the integrated head",
|
|
475
|
+
satisfied: hasValidFinalReview(facts)
|
|
476
|
+
}]
|
|
477
|
+
: [])
|
|
469
478
|
])
|
|
470
479
|
],
|
|
471
480
|
...(finalReviewAlternative.length === 0 ? {} : { alternatives: finalReviewAlternative }),
|
|
@@ -488,6 +497,7 @@ export function durableStateFingerprint(facts) {
|
|
|
488
497
|
...facts.workItems.map((item) => `work:${item.id}:${item.status}:${item.revision}:${item.updatedAt}`),
|
|
489
498
|
...facts.changeSets.map((changeSet) => `change-set:${changeSet.id}:${changeSet.headCommit}`),
|
|
490
499
|
...facts.integrations.map((attempt) => `integration:${attempt.id}:${attempt.status}:${attempt.updatedAt}`),
|
|
500
|
+
...facts.integrationQueueEntries.map((entry) => `integration-queue:${entry.id}:${entry.status}:${entry.updatedAt}`),
|
|
491
501
|
...facts.reviewRounds.map((round) => `review:${round.id}:${round.status}:${round.endedAt ?? ""}`),
|
|
492
502
|
...facts.taskFinalReviewContractEvents.map((event) => `task-final-review-event:${event.id}:${event.createdAt}`)
|
|
493
503
|
];
|
|
@@ -670,8 +680,7 @@ function taskFinalReviewContractResolution(facts) {
|
|
|
670
680
|
return resolveRecordedTaskFinalReviewContract(facts.task.id, facts.workItems, facts.reviewRounds, facts.taskFinalReviewContractEvents);
|
|
671
681
|
}
|
|
672
682
|
function taskFinalReviewRequired(facts) {
|
|
673
|
-
return taskFinalReviewContract(facts) !== undefined
|
|
674
|
-
|| latestTaskFinalReview(facts.reviewRounds) !== undefined;
|
|
683
|
+
return taskFinalReviewContract(facts) !== undefined;
|
|
675
684
|
}
|
|
676
685
|
function taskFinalReviewRole(facts) {
|
|
677
686
|
return taskFinalReviewContract(facts)?.reviewerRoleName
|
|
@@ -697,13 +706,10 @@ function latestTaskFinalReview(rounds) {
|
|
|
697
706
|
.reverse()
|
|
698
707
|
.find((round) => (round.scope ?? "work-item") === "task");
|
|
699
708
|
}
|
|
700
|
-
function hasCommittedIntegration(integrations, changeSetId) {
|
|
701
|
-
return integrations.some((attempt) => attempt.status === "committed" && attempt.changeSetIds.includes(changeSetId));
|
|
702
|
-
}
|
|
703
709
|
function needsChangeSetCapture(facts, item) {
|
|
704
710
|
if (item.status !== "completed")
|
|
705
711
|
return false;
|
|
706
|
-
if (facts.changeSets
|
|
712
|
+
if (governingChangeSets([item], facts.changeSets).length > 0)
|
|
707
713
|
return false;
|
|
708
714
|
const candidate = item.candidates.at(-1);
|
|
709
715
|
if (candidate === undefined)
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -120,8 +120,9 @@ Task type 描述需求意图,不选择执行协议。软件 Project 通常使
|
|
|
120
120
|
独立 owner,应先改为 feature 再创建 WorkItem。feature 由 Leader 判断是自己直接
|
|
121
121
|
交付,还是拆成由不同 Worker 独立负责、可并行推进的较大
|
|
122
122
|
WorkItem。实现步骤、测试、review finding 和局部修复都不是 WorkItem。只有当一项
|
|
123
|
-
需求本身具有独立 owner 和可验收结果时才创建 WorkItem
|
|
124
|
-
|
|
123
|
+
需求本身具有独立 owner 和可验收结果时才创建 WorkItem。只有当前 governing
|
|
124
|
+
Candidate 的 ChangeSet 是交付义务:它们必须通过 committed Integration 汇总回
|
|
125
|
+
Task main,或由 Leader 在队列中显式 supersede;旧 Candidate 和 ChangeSet 只保留为审计证据。
|
|
125
126
|
|
|
126
127
|
面向用户的时间默认按北京时间(`Asia/Shanghai`)显示;持久化记录和
|
|
127
128
|
`--json` 数据仍使用 UTC/RFC 3339。可通过以下命令查看或修改 IANA 时区:
|
|
@@ -475,6 +476,7 @@ Task Role 使用以下显式入口:
|
|
|
475
476
|
|
|
476
477
|
```sh
|
|
477
478
|
yui session enter <global-role>
|
|
479
|
+
yui session stop --all
|
|
478
480
|
yui task role view <task-id> <role>
|
|
479
481
|
yui task role takeover <task-id> <role>
|
|
480
482
|
yui task role release <task-id> <role>
|
|
@@ -482,12 +484,20 @@ yui task role release <task-id> <role>
|
|
|
482
484
|
|
|
483
485
|
`view` 始终只读。`takeover` 要求存在 active managed Run 且没有未决 Turn;它先以持久 CAS 把唯一 writer authority 转给人工 holder,再把相同 epoch 同步给 Agent Host,最后开放 PTY 输入网关。人工输入仍由 Host 转换为结构化 Provider Turn,而不是直接注入 Provider 终端。detach 会自动归还 authority;`release` 即使没有 active Run 也可执行,用于幂等修复中断或未完全同步的接管。Global Operator 与 global Role 继续使用原生交互式 CLI,不属于受管理 Task Provider 协议。
|
|
484
486
|
|
|
487
|
+
当新版本需要离线迁移 Home 时,应等待当前 Turn/Run 完成,然后从普通 shell
|
|
488
|
+
执行 `yui session stop --all`,再重新执行 `yui update`。停止命令会先整体预检:
|
|
489
|
+
只要仍有 Session 正在运行或存在未决生命周期工作,就不会开始停止;全部空闲
|
|
490
|
+
时会先阻止新的 Leader 调度,停止并等待 Controller 完全退出,重新检查运行时
|
|
491
|
+
事实后再停止 Task Role 和 global Role Session。成功后 Controller 保持停止,
|
|
492
|
+
应紧接着执行 `yui update`。如果当前安装版本还没有这条命令,应手动退出提示中
|
|
493
|
+
列出的全部 managed Session;新的 staged CLI 不能写入尚待迁移的旧 Home。
|
|
494
|
+
|
|
485
495
|
tmux 会在 pane 创建时固定其历史容量。配置该限制之前创建的 Role 会保留原容量;Yui 会在 Terminal attach 和 Web 中提示用户退出并重新进入一次,从而创建具有 100,000 行历史的新 pane。
|
|
486
496
|
|
|
487
|
-
每个 Role
|
|
488
|
-
Agent binding 独立保存 native session
|
|
489
|
-
|
|
490
|
-
|
|
497
|
+
每个 Role(包括 Operator)可绑定多个 Agent,但任一时刻只有一个 active Agent,
|
|
498
|
+
并为每个 Agent binding 独立保存 native session。同一种 adapter 可以有多个
|
|
499
|
+
binding,用于不同账号、模型、profile 或环境来源;这些 binding 是预先保存、
|
|
500
|
+
可随时切换的配置,而不是并行 writer。Operator 可为
|
|
491
501
|
每个 binding 保留多条历史对话。`operator new` 与 `operator resume`
|
|
492
502
|
复用唯一的 Operator tmux pane;存在运行中进程时,Yui 会先确认再停止
|
|
493
503
|
并切换。跨 Agent 切换默认复用已保存的 model/effort,只有用户明确选择
|
|
@@ -508,7 +518,7 @@ state、receipt 与 pane fence。Yui 不会解析 prompt glyph、进度文本、
|
|
|
508
518
|
或其他 Agent 终端输出来推断 ready 或 success。`captureRole()` 只用于显式的人类
|
|
509
519
|
transcript 查看,不具备生命周期权威。
|
|
510
520
|
|
|
511
|
-
稳定的 Role 上下文也属于启动元数据,而不是 bootstrap turn。Yui 通过 Agent 原生的 system/developer instruction 通道传入 Role 策略和 `systemPrompt`。Task execution Run 按角色接收通用 Leader 或 Worker Skill,review Run 则按持久 Run purpose 接收通用 Reviewer Skill;这些都只是 Yui 自己拥有的可移植编排规则。Project Skills 始终是 Project 中正常版本化的文件,由 Agent 通过自身项目机制发现、选择并按需加载;Yui 不扫描、不解析、不复制,也不注入 Project Skills。Codex developer instructions 只携带 Yui 自有 Role Skill
|
|
521
|
+
稳定的 Role 上下文也属于启动元数据,而不是 bootstrap turn。Yui 通过 Agent 原生的 system/developer instruction 通道传入 Role 策略和 `systemPrompt`。Task execution Run 按角色接收通用 Leader 或 Worker Skill,review Run 则按持久 Run purpose 接收通用 Reviewer Skill;这些都只是 Yui 自己拥有的可移植编排规则。Project Skills 始终是 Project 中正常版本化的文件,由 Agent 通过自身项目机制发现、选择并按需加载;Yui 不扫描、不解析、不复制,也不注入 Project Skills。Codex developer instructions 只携带 Yui 自有 Role Skill 的精简绝对路径,并作为本次 invocation 的覆盖值传入;已有的用户、profile、Project 和 system 配置不会使 Session 拒绝启动,Yui 也不会修改原配置文件。优先级高于 invocation 的 managed `developer_instructions` 仍会成为边界明确的启动阻塞,因为 Codex 不允许本次启动参数覆盖它。交互式 Codex Session 的结构化 `notify` 遵循同一规则:Doctor 会把普通覆盖来源作为上下文报告,并拒绝最终生效的 managed 冲突;Managed Run 不占用 `notify`,只使用 Agent Driver Hook。`skills.config` 只负责启停已发现 Skill,Yui 不会误用它。Claude 从 Yui 管理的私有 `0600` context 文件读取同一份 Yui Role Skill 内容,不再把大段或敏感文本放进 argv;重试和 resume 会复用按 purpose 区分的稳定路径。非 Operator 的 global Role 保持中性,不会注入 Task 编排 Skill。因此 Operator 会停在空白的原生 composer,用户输入仍是第一条 user message;Leader wake、Worker 和 Reviewer Run assignment 仍是邮箱投递的真实工作消息。不具备原生指令通道的 adapter 必须拒绝这类上下文,不能静默降级为首轮 user prompt。
|
|
512
522
|
|
|
513
523
|
## Controller 与失败处理
|
|
514
524
|
|
package/package.json
CHANGED
|
@@ -115,15 +115,14 @@ children through adapter metadata, and routes a later result reference through
|
|
|
115
115
|
the durable inbox. Do not poll, send a waiting Message, rewrite a checkpoint,
|
|
116
116
|
or yield merely to preserve that native wait.
|
|
117
117
|
|
|
118
|
-
Before the first durable Leader action, Yui
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
continues to apply.
|
|
118
|
+
Before the first durable Leader action, Yui observes fresh native generations
|
|
119
|
+
that produce no WorkItem, Review, Integration, or Leader-attributed durable
|
|
120
|
+
event. Two such generations create a non-blocking orchestration advisory for
|
|
121
|
+
Leader and Operator judgment; they do not fail the Role, reduce the configured
|
|
122
|
+
Provider retry policy, or prevent another useful generation. Read the evidence
|
|
123
|
+
before retrying, then choose whether to continue, change the configured Leader,
|
|
124
|
+
or perform direct maintenance without manufacturing protocol records merely to
|
|
125
|
+
silence the advisory.
|
|
127
126
|
|
|
128
127
|
Native child results have an explicit durability boundary. A native subagent is
|
|
129
128
|
best-effort by default: its result returns through the parent Conversation, and
|
|
@@ -293,8 +293,9 @@ terminal text. For progress, report:
|
|
|
293
293
|
- current Brief focus, latest Milestone, blockers, and open InputRequests.
|
|
294
294
|
|
|
295
295
|
Worker yield is not completion. Describe a result as awaiting Leader review
|
|
296
|
-
until it is accepted
|
|
297
|
-
ChangeSet is
|
|
296
|
+
until it is accepted. Report code as delivered only when the governing
|
|
297
|
+
Candidate's current ChangeSet is committed; a superseded disposition settles
|
|
298
|
+
the workflow without claiming that version was delivered.
|
|
298
299
|
|
|
299
300
|
## Enter and administer
|
|
300
301
|
|
|
@@ -340,11 +341,10 @@ ChangeSet is integrated.
|
|
|
340
341
|
Run, Agent, receipt, launch, and Session identities; never reconstruct them
|
|
341
342
|
from terminal text or ask the user to paste them.
|
|
342
343
|
- Retry only an explicitly failed recovery Job.
|
|
343
|
-
- When a Leader first-progress
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
maintenance action.
|
|
344
|
+
- When a Leader first-progress advisory is reported, inspect its native
|
|
345
|
+
generations and absence of durable progress. It is cost evidence rather than
|
|
346
|
+
a recovery gate: choose whether another generation, a different configured
|
|
347
|
+
Leader, or direct maintenance is the smallest useful next action.
|
|
348
348
|
- Use `yui task next-action <task>` and `yui execution audit` orchestration
|
|
349
349
|
advisories as read-only cost evidence. They may flag excess WorkItems,
|
|
350
350
|
repeated Reviews/checks, pre-progress generations, or terminal workspaces;
|