@zq-silk/yui 0.8.2 → 0.8.6
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 +40 -22
- package/README.md +61 -9
- package/dist/cli/commandCatalog.js +31 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +187 -22
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +648 -74
- package/dist/commands/taskCompletionGate.js +166 -2
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/runContextPack.js +19 -4
- package/dist/context/sessionBootstrapManifest.js +83 -2
- package/dist/controller/clientRuntime.js +7 -7
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +25 -5
- package/dist/executor/fileRoleLaunchPlanner.js +16 -11
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/gitWorkspace.js +7 -4
- package/dist/repository/taskBaseFreshness.js +4 -2
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +252 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +350 -0
- package/dist/run/agentRun.js +2 -2
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/firstProgressStopLoss.js +52 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/storage/sqliteStore.js +10 -1
- package/dist/storage/taskStore.js +12 -1
- package/dist/task/completionReadiness.js +91 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +146 -51
- package/dist/task/publicationReference.js +1 -0
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +12 -0
- package/dist/workspace/workItemChangeSetManager.js +2 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +79 -32
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +7 -2
|
@@ -18,6 +18,7 @@ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.j
|
|
|
18
18
|
import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
|
|
19
19
|
import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
|
|
20
20
|
import { UNSUPPORTED } from "./runtimeIdentity.js";
|
|
21
|
+
import { projectTaskOrchestration } from "./orchestrationMetrics.js";
|
|
21
22
|
export function createProductionExecutionAuditPorts() {
|
|
22
23
|
return {
|
|
23
24
|
openStore: (home) => openCompatibleFileTaskStore(home),
|
|
@@ -206,6 +207,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
206
207
|
events: section,
|
|
207
208
|
providerRetries: section,
|
|
208
209
|
workItems: section,
|
|
210
|
+
orchestration: section,
|
|
209
211
|
storage: section,
|
|
210
212
|
runtimeProtocol: section,
|
|
211
213
|
topLongRunning: section
|
|
@@ -443,7 +445,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
443
445
|
else if (round.deltaRecheck.disposition === "requires-full-review")
|
|
444
446
|
deltaEscalated += 1;
|
|
445
447
|
}
|
|
446
|
-
const classification = classifyReviewRound(round);
|
|
448
|
+
const classification = classifyReviewRound(round, store);
|
|
447
449
|
if (classification.faultClass === "review-infra")
|
|
448
450
|
infraFailed += 1;
|
|
449
451
|
else if (classification.faultClass === "review-semantic-negative") {
|
|
@@ -674,6 +676,37 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
674
676
|
return failed(error);
|
|
675
677
|
}
|
|
676
678
|
})();
|
|
679
|
+
const orchestration = (() => {
|
|
680
|
+
try {
|
|
681
|
+
const metrics = taskIds.flatMap((taskId) => {
|
|
682
|
+
const task = store.getTask(taskId);
|
|
683
|
+
if (task === null)
|
|
684
|
+
return [];
|
|
685
|
+
return [projectTaskOrchestration({
|
|
686
|
+
task,
|
|
687
|
+
runs: withinWindow(store.listAgentRuns(taskId), options),
|
|
688
|
+
roleSessionSets: sessionSetsWithinWindow(store.listRoleSessionSets(taskId), options),
|
|
689
|
+
workItems: withinWindow(store.listWorkItems(taskId), options),
|
|
690
|
+
changeSets: withinWindow(store.listChangeSets(taskId), options),
|
|
691
|
+
reviewRounds: withinWindow(store.listReviewRounds(taskId), options),
|
|
692
|
+
reviewFindings: withinWindow(store.listReviewFindings(taskId), options),
|
|
693
|
+
integrations: withinWindow(store.listIntegrationAttempts(taskId), options),
|
|
694
|
+
durableJobs: withinWindow(store.listDurableJobs(taskId), options),
|
|
695
|
+
publications: withinWindow(store.listPublicationReferences(taskId), options),
|
|
696
|
+
decisions: withinWindow(store.listDecisions(taskId), options),
|
|
697
|
+
events: withinWindow(store.listEvents(taskId), options),
|
|
698
|
+
managedWorkspaces: withinWindow(store.listManagedWorkspaces(taskId), options)
|
|
699
|
+
})];
|
|
700
|
+
});
|
|
701
|
+
return ok({
|
|
702
|
+
tasks: metrics,
|
|
703
|
+
advisoryCount: metrics.reduce((total, task) => total + task.advisories.length, 0)
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
return failed(error);
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
677
710
|
const storage = (() => {
|
|
678
711
|
try {
|
|
679
712
|
let stateJsonBytes = UNSUPPORTED;
|
|
@@ -834,8 +867,21 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
834
867
|
events,
|
|
835
868
|
providerRetries,
|
|
836
869
|
workItems,
|
|
870
|
+
orchestration,
|
|
837
871
|
storage,
|
|
838
872
|
runtimeProtocol,
|
|
839
873
|
topLongRunning
|
|
840
874
|
};
|
|
841
875
|
}
|
|
876
|
+
function withinWindow(records, options) {
|
|
877
|
+
return records.filter((record) => inWindow(record.createdAt, options));
|
|
878
|
+
}
|
|
879
|
+
function sessionSetsWithinWindow(sets, options) {
|
|
880
|
+
if (options.since === undefined && options.until === undefined)
|
|
881
|
+
return sets;
|
|
882
|
+
return sets.map((set) => ({
|
|
883
|
+
...set,
|
|
884
|
+
sessions: Object.fromEntries(Object.entries(set.sessions).filter(([, session]) => (inWindow(session.createdAt, options)))),
|
|
885
|
+
history: (set.history ?? []).filter((session) => inWindow(session.createdAt, options))
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
1
2
|
export const FAULT_CLASSES = [
|
|
2
3
|
"provider-transient",
|
|
3
4
|
"policy-denied",
|
|
@@ -73,15 +74,16 @@ export function classifyAgentRunFailure(run, structured) {
|
|
|
73
74
|
* Review execution failure (the Round itself failed to execute/deliver) is
|
|
74
75
|
* `review-infra`; a completed Round with failed checks is a semantic negative.
|
|
75
76
|
*/
|
|
76
|
-
export function classifyReviewRound(round) {
|
|
77
|
-
|
|
77
|
+
export function classifyReviewRound(round, evidence) {
|
|
78
|
+
const outcome = classifyReviewRoundOutcome(round, evidence);
|
|
79
|
+
if (outcome?.kind === "non-semantic") {
|
|
78
80
|
return {
|
|
79
81
|
faultClass: "review-infra",
|
|
80
82
|
basis: "structured",
|
|
81
|
-
evidence:
|
|
83
|
+
evidence: outcome.reason
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
|
-
if (
|
|
86
|
+
if (outcome?.kind === "semantic" && (round.checks ?? []).some((c) => c.outcome === "failed")) {
|
|
85
87
|
const failed = (round.checks ?? [])
|
|
86
88
|
.filter((c) => c.outcome === "failed")
|
|
87
89
|
.map((c) => c.name)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
2
|
+
import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
3
|
+
import { taskDeliveryPath } from "../task/task.js";
|
|
4
|
+
/** One Task's orchestration cost and advisory projection, with no writes. */
|
|
5
|
+
export function projectTaskOrchestration(facts) {
|
|
6
|
+
const evidence = {
|
|
7
|
+
listAgentRuns: () => facts.runs,
|
|
8
|
+
listReviewFindings: () => facts.reviewFindings,
|
|
9
|
+
listEvents: () => facts.events
|
|
10
|
+
};
|
|
11
|
+
const fullRounds = facts.reviewRounds.filter((round) => round.deltaRecheck === undefined);
|
|
12
|
+
const deltaRounds = facts.reviewRounds.filter((round) => round.deltaRecheck !== undefined);
|
|
13
|
+
const classifications = new Map(facts.reviewRounds.map((round) => [round.id, classifyReviewRoundOutcome(round, evidence)]));
|
|
14
|
+
const semanticRounds = facts.reviewRounds.filter((round) => (classifications.get(round.id)?.kind === "semantic"));
|
|
15
|
+
const p1P2Findings = facts.reviewFindings.filter((finding) => ((finding.severity === "p1" || finding.severity === "p2")
|
|
16
|
+
&& semanticRounds.some((round) => round.id === finding.firstReviewRoundId))).length;
|
|
17
|
+
const candidateTimes = [
|
|
18
|
+
...facts.changeSets.map(({ createdAt }) => createdAt),
|
|
19
|
+
...facts.workItems.flatMap((item) => item.candidates
|
|
20
|
+
.filter((candidate) => candidate.gitSnapshot !== undefined
|
|
21
|
+
|| candidate.taskMainSnapshot?.projects.some((project) => (project.baseCommit !== project.headCommit)))
|
|
22
|
+
.map(({ createdAt }) => createdAt))
|
|
23
|
+
].sort();
|
|
24
|
+
const firstCommitAt = candidateTimes[0];
|
|
25
|
+
const integrationIdentities = new Map();
|
|
26
|
+
for (const job of facts.durableJobs) {
|
|
27
|
+
if (job.owner.kind !== "integration-attempt")
|
|
28
|
+
continue;
|
|
29
|
+
const integrationAttemptId = job.owner.integrationAttemptId;
|
|
30
|
+
const attempt = facts.integrations.find(({ id }) => id === integrationAttemptId);
|
|
31
|
+
if (attempt === undefined || attempt.jobId !== job.id)
|
|
32
|
+
continue;
|
|
33
|
+
const identity = `${attempt.projectId}\0${job.head}\0${JSON.stringify(attempt.checkCommands)}`;
|
|
34
|
+
integrationIdentities.set(identity, (integrationIdentities.get(identity) ?? 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
const repeatedIdentities = [...integrationIdentities.values()]
|
|
37
|
+
.reduce((total, count) => total + Math.max(0, count - 1), 0);
|
|
38
|
+
const leaderSessions = facts.roleSessionSets.find(({ owner }) => owner.roleName === "leader") ?? null;
|
|
39
|
+
const firstProgress = projectFirstProgressStopLoss({
|
|
40
|
+
sessions: leaderSessions,
|
|
41
|
+
events: facts.events,
|
|
42
|
+
workItems: facts.workItems,
|
|
43
|
+
reviewRounds: facts.reviewRounds,
|
|
44
|
+
integrations: facts.integrations
|
|
45
|
+
});
|
|
46
|
+
const advisories = projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, firstProgress.exhausted);
|
|
47
|
+
const publicationAt = facts.task.completedAt === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: facts.publications
|
|
50
|
+
.map((reference) => reference.mergedAt ?? reference.createdAt)
|
|
51
|
+
.filter((timestamp) => timestamp <= facts.task.completedAt)
|
|
52
|
+
.sort()
|
|
53
|
+
.at(-1);
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
taskId: facts.task.id,
|
|
56
|
+
deliveryPath: taskDeliveryPath(facts.task),
|
|
57
|
+
timeToFirstProjectCommitMs: firstCommitAt === undefined
|
|
58
|
+
? null
|
|
59
|
+
: Math.max(0, Date.parse(firstCommitAt) - Date.parse(facts.task.createdAt)),
|
|
60
|
+
runs: {
|
|
61
|
+
total: facts.runs.length,
|
|
62
|
+
byStatus: counts(facts.runs.map(({ status }) => status)),
|
|
63
|
+
byRole: counts(facts.runs.map(({ roleName }) => roleName))
|
|
64
|
+
},
|
|
65
|
+
workItems: facts.workItems.length,
|
|
66
|
+
reviews: {
|
|
67
|
+
full: fullRounds.length,
|
|
68
|
+
delta: deltaRounds.length,
|
|
69
|
+
nonSemantic: [...classifications.values()].filter((value) => value?.kind === "non-semantic").length,
|
|
70
|
+
ambiguous: [...classifications.values()].filter((value) => value?.kind === "ambiguous").length,
|
|
71
|
+
p1P2Findings,
|
|
72
|
+
p1P2FindingsPerSemanticReview: semanticRounds.length === 0
|
|
73
|
+
? 0
|
|
74
|
+
: p1P2Findings / semanticRounds.length
|
|
75
|
+
},
|
|
76
|
+
integrations: {
|
|
77
|
+
attempts: facts.integrations.length,
|
|
78
|
+
failed: facts.integrations.filter(({ status }) => status === "failed").length,
|
|
79
|
+
repeatedIdentities,
|
|
80
|
+
evidenceReuses: facts.integrations.filter((attempt) => ((attempt.checks ?? []).some(({ details }) => details?.startsWith("Reused successful check evidence from ")))).length
|
|
81
|
+
},
|
|
82
|
+
providerGenerationsBeforeFirstProgress: firstProgress.generationsBeforeFirstProgress,
|
|
83
|
+
publicationToCompletionMs: publicationAt === undefined || facts.task.completedAt === undefined
|
|
84
|
+
? null
|
|
85
|
+
: Math.max(0, Date.parse(facts.task.completedAt) - Date.parse(publicationAt)),
|
|
86
|
+
terminalWorkspaceCount: terminalWorkspaceCount(facts),
|
|
87
|
+
advisories
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function projectAdvisories(facts, classifications, fullRounds, repeatedIdentities, stopLoss) {
|
|
91
|
+
const result = [];
|
|
92
|
+
if (taskDeliveryPath(facts.task) === "direct"
|
|
93
|
+
&& (facts.workItems.length > 0 || facts.reviewRounds.length > 0 || facts.integrations.length > 0)) {
|
|
94
|
+
result.push({
|
|
95
|
+
code: "direct-protocol-overhead",
|
|
96
|
+
reason: "Direct delivery accumulated WorkItem, Review, or Integration protocol overhead; keep the fix Leader-direct or explicitly promote it to integrated delivery.",
|
|
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}`)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const repairItems = facts.workItems.filter((item) => (item.acceptance.some((line) => line.startsWith("review-finding:"))));
|
|
113
|
+
const byRound = new Map();
|
|
114
|
+
for (const item of repairItems) {
|
|
115
|
+
const roundIds = new Set(item.acceptance.flatMap((line) => {
|
|
116
|
+
const findingId = line.startsWith("review-finding:") ? line.slice("review-finding:".length) : "";
|
|
117
|
+
const finding = facts.reviewFindings.find(({ id }) => id === findingId);
|
|
118
|
+
return finding === undefined ? [] : [finding.firstReviewRoundId];
|
|
119
|
+
}));
|
|
120
|
+
for (const roundId of roundIds)
|
|
121
|
+
byRound.set(roundId, [...(byRound.get(roundId) ?? []), item]);
|
|
122
|
+
}
|
|
123
|
+
for (const [roundId, items] of byRound) {
|
|
124
|
+
if (items.length < 2 || hasRepairFanoutDecision(facts.decisions, roundId, items))
|
|
125
|
+
continue;
|
|
126
|
+
result.push({
|
|
127
|
+
code: "review-repair-fanout",
|
|
128
|
+
reason: `Findings from Review ${roundId} were split across ${items.length} WorkItems without a durable Decision explaining independent ownership.`,
|
|
129
|
+
refs: [`review-round:${roundId}`, ...items.map(({ id }) => `work-item:${id}`)]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (repeatedIdentities > 0) {
|
|
133
|
+
result.push({
|
|
134
|
+
code: "repeated-integration-check",
|
|
135
|
+
reason: `${repeatedIdentities} Integration DurableJob(s) reran the same candidate commit and ordered checks.`,
|
|
136
|
+
refs: facts.durableJobs
|
|
137
|
+
.filter(({ owner }) => owner.kind === "integration-attempt")
|
|
138
|
+
.map(({ id }) => `durable-job:${id}`)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const semanticFull = fullRounds.filter((round) => classifications.get(round.id)?.kind === "semantic")
|
|
142
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
143
|
+
const recent = semanticFull.slice(-3);
|
|
144
|
+
const recentIds = new Set(recent.map(({ id }) => id));
|
|
145
|
+
const newFinding = facts.reviewFindings.some(({ firstReviewRoundId }) => recentIds.has(firstReviewRoundId));
|
|
146
|
+
if (semanticFull.length > 2 && !newFinding) {
|
|
147
|
+
result.push({
|
|
148
|
+
code: "review-budget-exhausted",
|
|
149
|
+
reason: `${semanticFull.length} full semantic Reviews ran and the latest three produced no new finding; stop repeating full rounds without a changed head or new risk.`,
|
|
150
|
+
refs: recent.map(({ id }) => `review-round:${id}`)
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (stopLoss) {
|
|
154
|
+
result.push({
|
|
155
|
+
code: "provider-first-progress-stop-loss",
|
|
156
|
+
reason: "Two fresh Leader generations produced no first durable progress; hand off to the unique Operator before another generation.",
|
|
157
|
+
refs: [facts.task.id]
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
function hasRepairFanoutDecision(decisions, roundId, items) {
|
|
163
|
+
return decisions.some((decision) => {
|
|
164
|
+
const text = `${decision.title}\n${decision.rationale}`;
|
|
165
|
+
return text.includes(roundId) && items.every(({ id }) => text.includes(id));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function terminalWorkspaceCount(facts) {
|
|
169
|
+
return facts.managedWorkspaces.filter(({ owner }) => {
|
|
170
|
+
if (owner.type === "task")
|
|
171
|
+
return false;
|
|
172
|
+
if (owner.type === "work-item") {
|
|
173
|
+
return terminalStatus(facts.workItems.find(({ id }) => id === owner.workItemId)?.status);
|
|
174
|
+
}
|
|
175
|
+
if (owner.type === "review-round") {
|
|
176
|
+
return terminalStatus(facts.reviewRounds.find(({ id }) => id === owner.reviewRoundId)?.status);
|
|
177
|
+
}
|
|
178
|
+
if (owner.type === "integration-attempt") {
|
|
179
|
+
return terminalStatus(facts.integrations.find(({ id }) => id === owner.integrationAttemptId)?.status);
|
|
180
|
+
}
|
|
181
|
+
if (owner.workItemId !== undefined) {
|
|
182
|
+
return terminalStatus(facts.workItems.find(({ id }) => id === owner.workItemId)?.status);
|
|
183
|
+
}
|
|
184
|
+
return owner.reviewRoundId !== undefined
|
|
185
|
+
&& terminalStatus(facts.reviewRounds.find(({ id }) => id === owner.reviewRoundId)?.status);
|
|
186
|
+
}).length;
|
|
187
|
+
}
|
|
188
|
+
function terminalStatus(status) {
|
|
189
|
+
return status !== undefined && !["pending", "running", "awaiting_acceptance", "blocked", "validating"].includes(status);
|
|
190
|
+
}
|
|
191
|
+
function counts(values) {
|
|
192
|
+
const result = {};
|
|
193
|
+
for (const value of values)
|
|
194
|
+
result[value] = (result[value] ?? 0) + 1;
|
|
195
|
+
return Object.freeze(result);
|
|
196
|
+
}
|
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import { normalizeRoleAgentSessionText, roleAgentSessionRef, validateRoleSessionSet } from "../executor/agentExecutor.js";
|
|
2
|
+
/** Separates the one selected writer authority from retained conversations. */
|
|
3
|
+
export function projectOperatorStatus(sessions, activeAgentId, activeAdapterId) {
|
|
4
|
+
if (sessions === null) {
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
writer: { state: "unrecorded", agentId: activeAgentId, adapterId: activeAdapterId },
|
|
7
|
+
historicalConversations: []
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
validateRoleSessionSet(sessions);
|
|
11
|
+
// The GlobalRole binding is the writer authority. The SessionSet pointer is
|
|
12
|
+
// retained operational state and may lag a Role update, so it cannot select
|
|
13
|
+
// a second Operator writer.
|
|
14
|
+
const active = sessions.sessions[activeAgentId];
|
|
15
|
+
const matchingActive = active?.adapterId === activeAdapterId ? active : undefined;
|
|
16
|
+
const activeRef = matchingActive === undefined ? undefined : operatorSessionRef(matchingActive);
|
|
17
|
+
const historicalConversations = listOperatorSessions(sessions)
|
|
18
|
+
.filter((entry) => entry.ref !== activeRef)
|
|
19
|
+
.map((entry) => ({ ...entry, state: "history" }));
|
|
20
|
+
if (matchingActive === undefined) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
writer: { state: "unrecorded", agentId: activeAgentId, adapterId: activeAdapterId },
|
|
23
|
+
historicalConversations
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
writer: {
|
|
28
|
+
state: matchingActive.status === "stopped" || matchingActive.status === "broken" ? "inactive" : "active",
|
|
29
|
+
agentId: matchingActive.agentId,
|
|
30
|
+
adapterId: matchingActive.adapterId,
|
|
31
|
+
sessionRef: activeRef,
|
|
32
|
+
nativeSessionId: matchingActive.nativeSessionId,
|
|
33
|
+
sessionStatus: matchingActive.status
|
|
34
|
+
},
|
|
35
|
+
historicalConversations
|
|
36
|
+
});
|
|
37
|
+
}
|
|
2
38
|
export function operatorSessionRef(session) {
|
|
3
39
|
return roleAgentSessionRef(session);
|
|
4
40
|
}
|
|
@@ -13,17 +13,19 @@
|
|
|
13
13
|
* the candidate read-only and reports dual-owner; a crashed activator resumes
|
|
14
14
|
* from the recorded phase.
|
|
15
15
|
*/
|
|
16
|
+
import { RELEASE_HANDOVER_PROMOTION_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
16
17
|
import { acquireHandoverLock, isOwnerLive, newHandoverId, readActiveReleasePointer, readCandidateDiscovery, readHandoverFence, readHandoverReceipt, readRuntimeIdentity, removeCandidateDiscovery, removeHandoverFence, writeActiveReleasePointer, writeHandoverFence, writeHandoverReceipt } from "./runtimeRelease.js";
|
|
17
18
|
export const DEFAULT_CANDIDATE_READY_TIMEOUT_MS = 30_000;
|
|
18
|
-
export const DEFAULT_PROMOTION_TIMEOUT_MS =
|
|
19
|
+
export const DEFAULT_PROMOTION_TIMEOUT_MS = RELEASE_HANDOVER_PROMOTION_TIMEOUT_MS;
|
|
19
20
|
export const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
20
21
|
/**
|
|
21
22
|
* Optional confirmation debounce after the candidate latches `dualOwner:
|
|
22
23
|
* true`. Defaults to 0: the candidate's own exit grace
|
|
23
|
-
* (`DEFAULT_DUAL_OWNER_GRACE_MS` in `handoverCandidate.ts
|
|
24
|
-
* authoritative old-owner exit window
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* (`DEFAULT_DUAL_OWNER_GRACE_MS` in `handoverCandidate.ts`) is the single
|
|
25
|
+
* authoritative old-owner exit window. It outlives the complete Controller
|
|
26
|
+
* shutdown/drain boundary, and the activator trusts the candidate's latched
|
|
27
|
+
* signal. A non-zero value only adds a short extra confirmation before reporting
|
|
28
|
+
* dual-owner; it must never be used to re-litigate the exit grace.
|
|
27
29
|
*/
|
|
28
30
|
export const DEFAULT_DUAL_OWNER_GRACE_MS = 0;
|
|
29
31
|
export async function activateRelease(ports, options) {
|
|
@@ -251,6 +251,21 @@ export function writeCandidateDiscovery(home, candidate) {
|
|
|
251
251
|
export function removeCandidateDiscovery(home) {
|
|
252
252
|
rmSync(join(resolve(home), CANDIDATE_DISCOVERY_PATH), { force: true });
|
|
253
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Read-only scheduler fence for the short release/rebind critical section.
|
|
256
|
+
* A stale owner does not block work; an unreadable lock fails closed for
|
|
257
|
+
* bounded Operator diagnosis instead of dispatching across an unknown fence.
|
|
258
|
+
*/
|
|
259
|
+
export function isHandoverLockHeld(home) {
|
|
260
|
+
const lockPath = join(resolve(home), "runtime", "handover.lock");
|
|
261
|
+
try {
|
|
262
|
+
const owner = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
263
|
+
return isHandoverLockLive(owner);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return !isEnoent(error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
254
269
|
export function acquireHandoverLock(home) {
|
|
255
270
|
const lockPath = join(resolve(home), "runtime", "handover.lock");
|
|
256
271
|
mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
@@ -15,15 +15,18 @@ export class RemoteBaselineConflictError extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
/** The small Git boundary used by project registration and Task workspaces. */
|
|
17
17
|
export class NodeGitWorkspace {
|
|
18
|
+
async resolveTree(repositoryPath, commit) {
|
|
19
|
+
return gitLine([
|
|
20
|
+
"-C", repositoryPath,
|
|
21
|
+
"rev-parse", "--verify", "--end-of-options", `${commit}^{tree}`
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
18
24
|
async findCommitWithSameTreeInHistory(input) {
|
|
19
25
|
const source = (await this.inspect(input.repositoryPath, input.sourceCommit)).baseCommit;
|
|
20
26
|
const history = (await this.inspect(input.repositoryPath, input.historyHead)).baseCommit;
|
|
21
27
|
if (await this.isAncestor(input.repositoryPath, source, history))
|
|
22
28
|
return source;
|
|
23
|
-
const sourceTree = await
|
|
24
|
-
"-C", input.repositoryPath,
|
|
25
|
-
"rev-parse", "--verify", "--end-of-options", `${source}^{tree}`
|
|
26
|
-
]);
|
|
29
|
+
const sourceTree = await this.resolveTree(input.repositoryPath, source);
|
|
27
30
|
const pageSize = 1000;
|
|
28
31
|
for (let skip = 0;; skip += pageSize) {
|
|
29
32
|
const output = await git([
|
|
@@ -107,10 +107,12 @@ export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
|
|
|
107
107
|
}));
|
|
108
108
|
return { taskId, refreshed: options.refresh === true, entries };
|
|
109
109
|
}
|
|
110
|
-
export function assertTaskBaseFreshnessForCompletion(report) {
|
|
110
|
+
export function assertTaskBaseFreshnessForCompletion(report, options = {}) {
|
|
111
111
|
const warnings = [];
|
|
112
112
|
for (const entry of report.entries) {
|
|
113
|
-
|
|
113
|
+
const acceptedPublishedTree = options.acceptedPublishedTreeProjectId === entry.projectId;
|
|
114
|
+
if ((entry.status === "behind" || entry.status === "diverged")
|
|
115
|
+
&& !acceptedPublishedTree) {
|
|
114
116
|
throw usageError(`Task ${report.taskId} Project ${entry.projectId} base is ${entry.status}; `
|
|
115
117
|
+ `run 'yui task base status ${report.taskId} --refresh' and choose an explicit delivery base. `
|
|
116
118
|
+ "Safe resolutions are to rebase or merge the Task workspace onto the refreshed remote base, "
|
|
@@ -108,6 +108,14 @@ export class TaskWorkspaceCoordinator {
|
|
|
108
108
|
await this.#stopLiveRoles(item.taskId, this.#workItemRoleNames(item));
|
|
109
109
|
return "released";
|
|
110
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Stops one Task Role's physical runtime without changing its workspace.
|
|
113
|
+
* The caller owns the subsequent atomic record retirement and wake.
|
|
114
|
+
*/
|
|
115
|
+
async cleanupTaskRoleRuntime(taskId, roleName) {
|
|
116
|
+
await this.#stopLiveRoles(taskId, [roleName]);
|
|
117
|
+
return "released";
|
|
118
|
+
}
|
|
111
119
|
async cleanupReviewRound(taskId, reviewRoundId) {
|
|
112
120
|
const round = this.store.getReviewRound(taskId, reviewRoundId);
|
|
113
121
|
if (round === null)
|
|
@@ -384,17 +392,12 @@ export class TaskWorkspaceCoordinator {
|
|
|
384
392
|
const observedPanes = inspect?.(taskId);
|
|
385
393
|
const live = targets.filter((roleName) => {
|
|
386
394
|
const sessions = this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
387
|
-
const activeSession = sessions === null
|
|
388
|
-
? undefined
|
|
389
|
-
: sessions.activeAgentId === undefined
|
|
390
|
-
// Narrow test doubles and restored callers predating activeAgentId
|
|
391
|
-
// still conservatively represent any nonterminal record as live.
|
|
392
|
-
? Object.values(sessions.sessions).find(({ status }) => status !== "stopped" && status !== "broken")
|
|
393
|
-
: sessions.sessions[sessions.activeAgentId];
|
|
394
395
|
return observedPanes?.some((pane) => pane.roleName === roleName && !pane.dead) === true
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
396
|
+
// A terminal current Session can still carry a resumable native id or
|
|
397
|
+
// Provider binding. Exact cleanup retires both before a workspace or
|
|
398
|
+
// release-control transition is allowed to wake this Role again.
|
|
399
|
+
|| (sessions !== null && (Object.keys(sessions.sessions).length > 0
|
|
400
|
+
|| sessions.providerBinding !== null));
|
|
398
401
|
});
|
|
399
402
|
if (live.length > 0)
|
|
400
403
|
await this.runtime.stopTaskRoleSessions(taskId, live);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { deltaRecheckMaxChangedFiles, deltaRecheckMaxChangedLines } from "./reviewConfig.js";
|
|
3
3
|
import { validateDeltaRecheckRecord } from "./reviewRound.js";
|
|
4
|
+
import { isSemanticReviewRound } from "./reviewOutcomeClassifier.js";
|
|
4
5
|
/**
|
|
5
6
|
* Assesses whether a delta-recheck may be attempted. Every deterministic
|
|
6
7
|
* gate fails closed here; semantic equivalence is always left to the
|
|
@@ -8,11 +9,11 @@ import { validateDeltaRecheckRecord } from "./reviewRound.js";
|
|
|
8
9
|
*/
|
|
9
10
|
export async function assessDeltaRecheck(input) {
|
|
10
11
|
const { repositoryPaths, previousRound, candidate, git, config } = input;
|
|
11
|
-
if (previousRound
|
|
12
|
+
if (!isSemanticReviewRound(previousRound)
|
|
12
13
|
|| (previousRound.scope ?? "work-item") !== "task") {
|
|
13
14
|
return {
|
|
14
15
|
kind: "ineligible",
|
|
15
|
-
reason: "Delta recheck requires a completed Task-final ReviewRound."
|
|
16
|
+
reason: "Delta recheck requires a semantic completed Task-final ReviewRound."
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
if (previousRound.taskCandidate === undefined) {
|
|
@@ -117,11 +117,11 @@ export function reconcileReviewFindings(store, taskId, roundId, now) {
|
|
|
117
117
|
if (round === null) {
|
|
118
118
|
return { roundId, skipped: true, reason: "ReviewRound not found.", created: [], updated: [], conflicts: [] };
|
|
119
119
|
}
|
|
120
|
-
if (!isSemanticReviewRound(round)) {
|
|
120
|
+
if (!isSemanticReviewRound(round, store)) {
|
|
121
121
|
return {
|
|
122
122
|
roundId,
|
|
123
123
|
skipped: true,
|
|
124
|
-
reason: "ReviewRound is
|
|
124
|
+
reason: "ReviewRound is non-semantic or ambiguous, not a proven semantic report.",
|
|
125
125
|
created: [],
|
|
126
126
|
updated: [],
|
|
127
127
|
conflicts: []
|
|
@@ -464,7 +464,7 @@ export function renderFindingLedgerContext(summary) {
|
|
|
464
464
|
export function reusableTaskReviewEvidence(store, taskId, candidate) {
|
|
465
465
|
const rounds = store.listReviewRounds(taskId)
|
|
466
466
|
.filter((round) => (round.scope ?? "work-item") === "task"
|
|
467
|
-
&& round
|
|
467
|
+
&& isSemanticReviewRound(round, store)
|
|
468
468
|
&& round.evidenceCommit !== undefined
|
|
469
469
|
&& isSameTaskReviewCandidate(round.taskCandidate, candidate))
|
|
470
470
|
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
|
|
@@ -492,7 +492,8 @@ export function reusableTaskReviewEvidence(store, taskId, candidate) {
|
|
|
492
492
|
*/
|
|
493
493
|
export function buildTaskFinalReviewFindingContext(store, taskId, candidate) {
|
|
494
494
|
const previousSemanticRound = store.listReviewRounds(taskId)
|
|
495
|
-
.filter((round) => (round.scope ?? "work-item") === "task"
|
|
495
|
+
.filter((round) => (round.scope ?? "work-item") === "task"
|
|
496
|
+
&& isSemanticReviewRound(round, store))
|
|
496
497
|
.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))
|
|
497
498
|
.at(-1) ?? null;
|
|
498
499
|
const reusableEvidence = reusableTaskReviewEvidence(store, taskId, candidate);
|