@zq-silk/yui 0.8.9 → 0.10.0
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 +48 -46
- package/README.md +72 -42
- package/dist/cli/commandCatalog.js +16 -8
- package/dist/cli.js +27 -32
- package/dist/commands/executionAuditCommands.js +2 -2
- 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 +123 -340
- package/dist/commands/taskCompletionGate.js +15 -12
- package/dist/commands/taskContextCommand.js +18 -11
- package/dist/commands/taskNextActionCommand.js +4 -5
- package/dist/commands/taskWorkspaceCommands.js +2 -2
- package/dist/controller/clientRuntime.js +65 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2 -49
- package/dist/doctor/doctor.js +16 -12
- package/dist/execution/executionGroup.js +0 -3
- package/dist/executor/agentAdapter.js +39 -42
- package/dist/executor/agentExecutor.js +4 -2
- package/dist/executor/codexConfigConflict.js +40 -16
- package/dist/executor/effectiveLaunch.js +33 -3
- package/dist/executor/fileRoleLaunchPlanner.js +15 -24
- package/dist/integration/deliveryObligation.js +72 -0
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/lifecycle/exactRunTerminalization.js +12 -8
- package/dist/observability/orchestrationMetrics.js +12 -26
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/taskBaseFreshness.js +5 -11
- package/dist/repository/taskWorkspaceCoordinator.js +2 -0
- package/dist/repository/taskWorkspacePreparer.js +173 -26
- package/dist/review/reviewRound.js +41 -24
- package/dist/role/role.js +0 -9
- package/dist/runtime/{firstProgressStopLoss.js → firstProgressAdvisory.js} +7 -21
- package/dist/scheduler/leaderWakeupProcessor.js +0 -25
- package/dist/setup/setupCommand.js +0 -5
- package/dist/storage/migration/productionRegistry.js +138 -0
- package/dist/storage/sqliteStore.js +2 -1
- package/dist/storage/taskStore.js +27 -27
- package/dist/storage/upgrade/upgradeOrchestrator.js +35 -7
- package/dist/task/completionReadiness.js +35 -25
- package/dist/task/nextAction.js +70 -78
- package/dist/task/task.js +12 -19
- package/dist/web/assets/client/components.js +3 -1
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/webSnapshot.js +0 -3
- package/i18n/README.zh-CN.md +33 -24
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +55 -52
- package/skills/yui-operator/SKILL.md +31 -40
- package/skills/yui-reviewer/SKILL.md +18 -12
- package/skills/yui-worker/SKILL.md +5 -3
|
@@ -4,15 +4,14 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import { parse } from "smol-toml";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* layers
|
|
10
|
-
*
|
|
7
|
+
* Reports the local, file-backed Codex configuration layers supported by Yui.
|
|
8
|
+
* Yui's invocation-local launch settings take precedence over ordinary local
|
|
9
|
+
* layers without mutating those files. Higher-precedence managed policy is
|
|
10
|
+
* reported separately because an invocation cannot override it. Remote
|
|
11
|
+
* managed layers remain outside this reporting boundary. Project files are
|
|
12
|
+
* considered only when the directory, project root, or repository root is
|
|
13
|
+
* trusted.
|
|
11
14
|
*/
|
|
12
|
-
export function inspectCodexDeveloperInstructions(input) {
|
|
13
|
-
return inspectCodexConfigKeys(input, ["developer_instructions"])
|
|
14
|
-
.developerInstructions;
|
|
15
|
-
}
|
|
16
15
|
export function inspectCodexLaunchConfig(input) {
|
|
17
16
|
return inspectCodexConfigKeys(input, ["developer_instructions", "notify"]);
|
|
18
17
|
}
|
|
@@ -49,12 +48,17 @@ function inspectCodexConfigKeys(input, keys) {
|
|
|
49
48
|
? withExactWorkspaceTrust(discovery, input.workspace)
|
|
50
49
|
: discovery);
|
|
51
50
|
const candidates = [
|
|
52
|
-
...discoveryPaths.map((path) => ({
|
|
51
|
+
...discoveryPaths.map((path) => ({
|
|
52
|
+
path,
|
|
53
|
+
keys,
|
|
54
|
+
precedence: "session-overridable"
|
|
55
|
+
})),
|
|
53
56
|
...projectPaths.map((path) => ({
|
|
54
57
|
path,
|
|
55
|
-
keys: keys.filter((key) => key === "developer_instructions")
|
|
58
|
+
keys: keys.filter((key) => key === "developer_instructions"),
|
|
59
|
+
precedence: "session-overridable"
|
|
56
60
|
})),
|
|
57
|
-
{ path: managedPath, keys }
|
|
61
|
+
{ path: managedPath, keys, precedence: "managed" }
|
|
58
62
|
];
|
|
59
63
|
let developerInstructions = { status: "absent" };
|
|
60
64
|
let notify = { status: "absent" };
|
|
@@ -71,16 +75,36 @@ function inspectCodexConfigKeys(input, keys) {
|
|
|
71
75
|
catch (error) {
|
|
72
76
|
throw unreliableInspection(candidate.path, error);
|
|
73
77
|
}
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
if (configured.has("developer_instructions")) {
|
|
79
|
+
developerInstructions = {
|
|
80
|
+
status: "configured",
|
|
81
|
+
source: candidate.path,
|
|
82
|
+
precedence: candidate.precedence
|
|
83
|
+
};
|
|
77
84
|
}
|
|
78
|
-
if (
|
|
79
|
-
notify = {
|
|
85
|
+
if (configured.has("notify")) {
|
|
86
|
+
notify = {
|
|
87
|
+
status: "configured",
|
|
88
|
+
source: candidate.path,
|
|
89
|
+
precedence: candidate.precedence
|
|
90
|
+
};
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
return { developerInstructions, notify };
|
|
83
94
|
}
|
|
95
|
+
export function assertCodexLaunchOverridesAvailable(inspection, keys) {
|
|
96
|
+
for (const key of keys) {
|
|
97
|
+
const configured = inspection[key];
|
|
98
|
+
if (configured.status !== "configured" || configured.precedence !== "managed") {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const nativeKey = key === "developerInstructions"
|
|
102
|
+
? "developer_instructions"
|
|
103
|
+
: "notify";
|
|
104
|
+
throw new Error(`Codex ${nativeKey} is controlled by higher-precedence managed configuration at `
|
|
105
|
+
+ `${configured.source}; Yui cannot apply its invocation-local ${nativeKey} value.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
84
108
|
function withExactWorkspaceTrust(discovery, workspace) {
|
|
85
109
|
return {
|
|
86
110
|
...discovery,
|
|
@@ -84,7 +84,9 @@ export function effectiveLaunchSnapshotsCompatible(existing, desired) {
|
|
|
84
84
|
* configuration with which it started. A later Run may resume that exact
|
|
85
85
|
* Session only when the current durable Task workspace proves that every
|
|
86
86
|
* non-commit workspace identity and every other launch field is unchanged.
|
|
87
|
-
*
|
|
87
|
+
* A Task-final Reviewer workspace is also mutable between semantic Rounds: it
|
|
88
|
+
* keeps one Role-owned physical identity while its frozen commits and Round
|
|
89
|
+
* identity advance. WorkItem and ExecutionLane workspaces remain strict.
|
|
88
90
|
*/
|
|
89
91
|
export function effectiveLaunchSnapshotsCompatibleForTaskMain(existing, desired, workspace) {
|
|
90
92
|
if (effectiveLaunchSnapshotsCompatible(existing, desired))
|
|
@@ -94,16 +96,34 @@ export function effectiveLaunchSnapshotsCompatibleForTaskMain(existing, desired,
|
|
|
94
96
|
if (workspace === null || workspace === undefined)
|
|
95
97
|
return false;
|
|
96
98
|
validateManagedWorkspace(workspace);
|
|
97
|
-
if (workspace.owner.type !== "task")
|
|
98
|
-
return false;
|
|
99
99
|
const durableWorkspace = {
|
|
100
100
|
root: workspace.root,
|
|
101
101
|
entries: workspace.entries.map((entry) => ({ ...entry }))
|
|
102
102
|
};
|
|
103
103
|
if (!isDeepStrictEqual(desired.workspace, durableWorkspace))
|
|
104
104
|
return false;
|
|
105
|
+
if (workspace.owner.type === "review-round") {
|
|
106
|
+
return effectiveLaunchSnapshotsCompatibleForTaskReview(existing, desired);
|
|
107
|
+
}
|
|
108
|
+
if (workspace.owner.type !== "task")
|
|
109
|
+
return false;
|
|
105
110
|
return isDeepStrictEqual(taskMainCompatibleSnapshot(existing), taskMainCompatibleSnapshot(desired));
|
|
106
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Same Task Reviewer Role, same stable physical workspace and launch policy,
|
|
114
|
+
* but a new semantic ReviewRound and frozen head. The native Session keeps its
|
|
115
|
+
* conversation; each AgentRun still records the new exact Round snapshot.
|
|
116
|
+
*/
|
|
117
|
+
export function effectiveLaunchSnapshotsCompatibleForTaskReview(existing, desired) {
|
|
118
|
+
validateEffectiveLaunchSnapshot(existing);
|
|
119
|
+
validateEffectiveLaunchSnapshot(desired);
|
|
120
|
+
if (existing.reviewRoundId === undefined
|
|
121
|
+
|| existing.reviewBaseCommit === undefined
|
|
122
|
+
|| desired.reviewRoundId === undefined
|
|
123
|
+
|| desired.reviewBaseCommit === undefined)
|
|
124
|
+
return false;
|
|
125
|
+
return isDeepStrictEqual(taskReviewCompatibleSnapshot(existing), taskReviewCompatibleSnapshot(desired));
|
|
126
|
+
}
|
|
107
127
|
/** Preserves a fixed Session's launch configuration while freezing fresh Task-main Git facts. */
|
|
108
128
|
export function effectiveLaunchWithTaskMainWorkspace(existing, workspace) {
|
|
109
129
|
validateEffectiveLaunchSnapshot(existing);
|
|
@@ -129,6 +149,16 @@ function taskMainCompatibleSnapshot(snapshot) {
|
|
|
129
149
|
}
|
|
130
150
|
};
|
|
131
151
|
}
|
|
152
|
+
function taskReviewCompatibleSnapshot(snapshot) {
|
|
153
|
+
const { sourceDesiredRevision: _sourceDesiredRevision, reviewRoundId: _reviewRoundId, reviewBaseCommit: _reviewBaseCommit, workspace, ...launch } = snapshot;
|
|
154
|
+
return {
|
|
155
|
+
...launch,
|
|
156
|
+
workspace: {
|
|
157
|
+
root: workspace.root,
|
|
158
|
+
entries: workspace.entries.map(({ baseCommit: _baseCommit, baseRef: _baseRef, ...entry }) => entry)
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
132
162
|
export function validateEffectiveLaunchSnapshot(snapshot) {
|
|
133
163
|
if (snapshot.schemaVersion !== 2) {
|
|
134
164
|
throw new Error("Effective launch snapshot must use schemaVersion 2.");
|
|
@@ -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
|
+
}
|
|
@@ -675,7 +675,7 @@ async function integrationCommitPlan(store, taskId, repositoryPath, changeSetIds
|
|
|
675
675
|
`${changeSet.baseCommit}..${changeSet.headCommit}`
|
|
676
676
|
])).trim().split("\n").filter(Boolean);
|
|
677
677
|
for (const commit of commits) {
|
|
678
|
-
// A
|
|
678
|
+
// A Task-main recovery may capture commits after they are already
|
|
679
679
|
// present on the exact target. Treat those commits as applied rather
|
|
680
680
|
// than attempting an empty cherry-pick; the later checks and CAS still
|
|
681
681
|
// fence the committed Integration to expectedHead.
|
|
@@ -83,19 +83,23 @@ export function validateExactRunReviewRound(store, run, options = {}) {
|
|
|
83
83
|
return { disposition: "obsolete", round, reason: "review-lane-workspace-lineage-mismatch" };
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
86
|
+
const task = store.getTask(run.taskId);
|
|
87
|
+
const taskScope = (round.scope ?? "work-item") === "task";
|
|
88
|
+
const item = taskScope || round.workItemId === undefined
|
|
89
|
+
? null
|
|
90
|
+
: store.getWorkItem(run.taskId, round.workItemId);
|
|
91
|
+
if (!taskScope && item === null) {
|
|
88
92
|
return { disposition: "obsolete", round, reason: "review-work-item-missing" };
|
|
89
93
|
}
|
|
90
|
-
const candidate =
|
|
91
|
-
|
|
94
|
+
const candidate = taskScope
|
|
95
|
+
? undefined
|
|
96
|
+
: item.candidates.find(({ id }) => id === round.candidateId);
|
|
97
|
+
if (!taskScope && candidate === undefined) {
|
|
92
98
|
return { disposition: "obsolete", round, reason: "review-candidate-missing" };
|
|
93
99
|
}
|
|
94
|
-
const task = store.getTask(run.taskId);
|
|
95
|
-
const taskScope = (round.scope ?? "work-item") === "task";
|
|
96
100
|
const frozenProjects = taskScope
|
|
97
101
|
? round.taskCandidate?.projects
|
|
98
|
-
: candidate
|
|
102
|
+
: candidate?.gitSnapshot?.projects;
|
|
99
103
|
if (taskScope) {
|
|
100
104
|
if (task === null || round.taskCandidate === undefined) {
|
|
101
105
|
return { disposition: "obsolete", round, reason: "review-task-candidate-missing" };
|
|
@@ -107,7 +111,7 @@ export function validateExactRunReviewRound(store, run, options = {}) {
|
|
|
107
111
|
return { disposition: "obsolete", round, reason: "review-frozen-project-scope-drift" };
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
|
-
else if (candidate
|
|
114
|
+
else if (candidate?.gitSnapshot !== undefined
|
|
111
115
|
&& candidate.gitSnapshot.reviewBaseCommit !== round.reviewBaseCommit) {
|
|
112
116
|
return { disposition: "obsolete", round, reason: "review-candidate-snapshot-drift" };
|
|
113
117
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
2
|
-
import {
|
|
3
|
-
import { taskDeliveryPath } from "../task/task.js";
|
|
2
|
+
import { projectFirstProgressAdvisory } from "../runtime/firstProgressAdvisory.js";
|
|
4
3
|
/** One Task's orchestration cost and advisory projection, with no writes. */
|
|
5
4
|
export function projectTaskOrchestration(facts) {
|
|
6
5
|
const evidence = {
|
|
@@ -36,14 +35,14 @@ export function projectTaskOrchestration(facts) {
|
|
|
36
35
|
const repeatedIdentities = [...integrationIdentities.values()]
|
|
37
36
|
.reduce((total, count) => total + Math.max(0, count - 1), 0);
|
|
38
37
|
const leaderSessions = facts.roleSessionSets.find(({ owner }) => owner.roleName === "leader") ?? null;
|
|
39
|
-
const firstProgress =
|
|
38
|
+
const firstProgress = projectFirstProgressAdvisory({
|
|
40
39
|
sessions: leaderSessions,
|
|
41
40
|
events: facts.events,
|
|
42
41
|
workItems: facts.workItems,
|
|
43
42
|
reviewRounds: facts.reviewRounds,
|
|
44
43
|
integrations: facts.integrations
|
|
45
44
|
});
|
|
46
|
-
const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.
|
|
45
|
+
const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.attentionRecommended);
|
|
47
46
|
const publicationAt = facts.task.completedAt === undefined
|
|
48
47
|
? undefined
|
|
49
48
|
: facts.publications
|
|
@@ -53,7 +52,7 @@ export function projectTaskOrchestration(facts) {
|
|
|
53
52
|
.at(-1);
|
|
54
53
|
return Object.freeze({
|
|
55
54
|
taskId: facts.task.id,
|
|
56
|
-
|
|
55
|
+
taskType: facts.task.type ?? null,
|
|
57
56
|
timeToFirstProjectCommitMs: firstCommitAt === undefined
|
|
58
57
|
? null
|
|
59
58
|
: Math.max(0, Date.parse(firstCommitAt) - Date.parse(facts.task.createdAt)),
|
|
@@ -87,26 +86,13 @@ export function projectTaskOrchestration(facts) {
|
|
|
87
86
|
advisories
|
|
88
87
|
});
|
|
89
88
|
}
|
|
90
|
-
function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities,
|
|
89
|
+
function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgressAttention) {
|
|
91
90
|
const result = [];
|
|
92
|
-
if (
|
|
93
|
-
&& (facts.workItems.length > 0 || facts.reviewRounds.length > 0 || facts.integrations.length > 0)) {
|
|
91
|
+
if (facts.task.type === "bugfix" && facts.workItems.length > 0) {
|
|
94
92
|
result.push({
|
|
95
|
-
code: "
|
|
96
|
-
reason:
|
|
97
|
-
refs:
|
|
98
|
-
...facts.workItems.map(({ id }) => `work-item:${id}`),
|
|
99
|
-
...facts.reviewRounds.map(({ id }) => `review-round:${id}`),
|
|
100
|
-
...facts.integrations.map(({ id }) => `integration-attempt:${id}`)
|
|
101
|
-
]
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
const initial = facts.workItems.filter(({ dependsOn }) => dependsOn.length === 0);
|
|
105
|
-
if (taskDeliveryPath(facts.task) === "integrated" && initial.length > 1) {
|
|
106
|
-
result.push({
|
|
107
|
-
code: "guarded-workitem-fanout",
|
|
108
|
-
reason: `${initial.length} initial WorkItems were created for an integrated Task; start with one bounded fix unless independence is explicit.`,
|
|
109
|
-
refs: initial.map(({ id }) => `work-item:${id}`)
|
|
93
|
+
code: "bugfix-workitem-overhead",
|
|
94
|
+
reason: `Bugfix ${facts.task.id} created ${facts.workItems.length} WorkItem(s); bugfixes are Leader-owned, so reclassify expanding scope as a feature before delegating independent delivery units.`,
|
|
95
|
+
refs: facts.workItems.map(({ id }) => `work-item:${id}`)
|
|
110
96
|
});
|
|
111
97
|
}
|
|
112
98
|
const repairItems = facts.workItems.filter((item) => (item.acceptance.some((line) => line.startsWith("review-finding:"))));
|
|
@@ -150,10 +136,10 @@ function projectAdvisories(facts, classifications, fullRounds, repeatedIdentitie
|
|
|
150
136
|
refs: recent.map(({ id }) => `review-round:${id}`)
|
|
151
137
|
});
|
|
152
138
|
}
|
|
153
|
-
if (
|
|
139
|
+
if (firstProgressAttention) {
|
|
154
140
|
result.push({
|
|
155
|
-
code: "provider-first-progress-
|
|
156
|
-
reason: "Two fresh Leader generations produced no first durable progress;
|
|
141
|
+
code: "provider-first-progress-advisory",
|
|
142
|
+
reason: "Two fresh Leader generations produced no first durable progress; consider Operator attention before another generation.",
|
|
157
143
|
refs: [facts.task.id]
|
|
158
144
|
});
|
|
159
145
|
}
|
|
@@ -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
|
|
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 a compatible Reviewer Session may continue across changed-head Rounds without reusing an earlier verdict. Do not turn speculative or extreme edge cases into new state, retries, fallbacks, or protocol. In a ReviewRound-owned 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
|
];
|
|
@@ -113,23 +113,17 @@ export function assertTaskBaseFreshnessForCompletion(report, options = {}) {
|
|
|
113
113
|
const acceptedPublishedTree = options.acceptedPublishedTreeProjectId === entry.projectId;
|
|
114
114
|
if ((entry.status === "behind" || entry.status === "diverged")
|
|
115
115
|
&& !acceptedPublishedTree) {
|
|
116
|
-
|
|
117
|
-
+
|
|
118
|
-
+ "Safe resolutions are to rebase or merge the Task workspace onto the refreshed remote base, "
|
|
119
|
-
+ "create a clean delivery branch from that base and cherry-pick the Task changes, "
|
|
120
|
-
+ "or ask the Leader to resolve it. Continuing without rebasing is allowed only after explicitly "
|
|
121
|
-
+ "recording the unrelated-diff risk.");
|
|
116
|
+
warnings.push(`Project ${entry.projectId} base is ${entry.status}; the Leader must choose the delivery base `
|
|
117
|
+
+ "and account for any remote-only changes.");
|
|
122
118
|
}
|
|
123
119
|
if (entry.workspaceClean === false) {
|
|
124
120
|
throw usageError(`Task ${report.taskId} Project ${entry.projectId} workspace is dirty; `
|
|
125
121
|
+ "commit, integrate, or clean it before completion.");
|
|
126
122
|
}
|
|
127
123
|
if (entry.status === "unknown") {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
warnings.push(`Project ${entry.projectId} remote base is unknown; local completion continues without a remote claim.`);
|
|
124
|
+
warnings.push(report.refreshed
|
|
125
|
+
? `Project ${entry.projectId} remote base could not be refreshed: ${entry.error ?? "unknown error"}; local completion continues without a remote claim.`
|
|
126
|
+
: `Project ${entry.projectId} remote base is unknown; local completion continues without a remote claim.`);
|
|
133
127
|
}
|
|
134
128
|
}
|
|
135
129
|
return warnings;
|
|
@@ -123,6 +123,8 @@ export class TaskWorkspaceCoordinator {
|
|
|
123
123
|
if (round.status !== "completed" && round.status !== "failed") {
|
|
124
124
|
throw new Error(`ReviewRound must be terminal before cleanup: ${round.id}.`);
|
|
125
125
|
}
|
|
126
|
+
if (round.workspaceDisposition?.kind === "reassigned")
|
|
127
|
+
return "missing";
|
|
126
128
|
// Hold the per-Project maintenance fence so a concurrent migrate/rebuild/
|
|
127
129
|
// archive cannot interleave with worktree removal.
|
|
128
130
|
const workspace = this.store.getReviewRoundWorkspace(taskId, reviewRoundId);
|