@zq-silk/yui 0.9.0 → 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 +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/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
package/ARCHITECTURE.md
CHANGED
|
@@ -98,9 +98,9 @@ leaves every candidate for the Leader to accept directly or review explicitly.
|
|
|
98
98
|
`final` keeps WorkItem acceptance and Integration independent and supplies the
|
|
99
99
|
default Reviewer Role when the Leader decides the frozen Task result warrants
|
|
100
100
|
an independent final Review. An immutable Task-final contract can require that
|
|
101
|
-
Review.
|
|
102
|
-
Task head
|
|
103
|
-
final Reviewer evaluates the whole
|
|
101
|
+
Review. A Leader-requested Round remains evidence without becoming policy: a
|
|
102
|
+
later Task head does not require another Round unless the Leader requests one
|
|
103
|
+
or an explicit Task contract requires it. This final Reviewer evaluates the whole
|
|
104
104
|
Task, so normal delivery does not pay for a complete review of every WorkItem.
|
|
105
105
|
Review Runs complete only their exact ReviewRound, leave the WorkItem awaiting
|
|
106
106
|
acceptance, and never trigger another review or append a Candidate. Successful
|
package/README.md
CHANGED
|
@@ -144,6 +144,21 @@ Input by itself do not block. A blocker reports the total and exact available
|
|
|
144
144
|
Task/Role/Run/native-session/launch identities and reason, leaves the scene
|
|
145
145
|
unchanged, and tells the user to re-run `yui update` after the listed work clears;
|
|
146
146
|
it never kills, resets, rebinds, retries, or drains on the user's behalf.
|
|
147
|
+
After every listed Turn or Run finishes, the user can stop all idle managed
|
|
148
|
+
Sessions from a normal shell and retry the update:
|
|
149
|
+
|
|
150
|
+
```sh
|
|
151
|
+
yui session stop --all
|
|
152
|
+
yui update
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`session stop --all` first checks every current Session and refuses without
|
|
156
|
+
stopping anything while any Session still has a running Turn/Run or pending
|
|
157
|
+
lifecycle work. Once clear, it fences new Leader dispatch, stops and drains the
|
|
158
|
+
Controller, rechecks the runtime facts, then stops every exact idle Session. The
|
|
159
|
+
Controller remains stopped so `yui update` can enter its offline window. When the
|
|
160
|
+
installed version predates this command, exit every listed managed Session
|
|
161
|
+
manually instead; a staged new CLI cannot write the older migration-required Home.
|
|
147
162
|
|
|
148
163
|
Only that user re-run may enter the existing full migration. Once its preflight
|
|
149
164
|
is clear, the update parent captures and stops the exact old Controller PID. The
|
|
@@ -266,9 +281,11 @@ feature before creating WorkItems. The Leader decides whether a feature is
|
|
|
266
281
|
small enough to deliver on Task main or large enough for independently owned WorkItems. A WorkItem is
|
|
267
282
|
one substantial requirement for one Worker, not a development step, test run,
|
|
268
283
|
review finding, or local fix. Multiple WorkItems are useful only when distinct
|
|
269
|
-
Workers can advance meaningful requirements independently.
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
Workers can advance meaningful requirements independently. A WorkItem's
|
|
285
|
+
governing Candidate defines its delivery obligation: its current ChangeSets
|
|
286
|
+
must reach committed Integration or an explicit superseded queue disposition
|
|
287
|
+
before Task-final Review or completion. Older Candidate and ChangeSet records
|
|
288
|
+
remain audit evidence without keeping the Task open.
|
|
272
289
|
|
|
273
290
|
`project refresh` is the explicit network operation for a stable Project checkout. It fetches the
|
|
274
291
|
configured stable branch directly from the Project remote URL and advances only through a clean,
|
|
@@ -402,7 +419,9 @@ yui task base status <task-id> --refresh
|
|
|
402
419
|
The default check is offline and uses local remote-tracking refs. `--refresh`
|
|
403
420
|
is the explicit authorization to query the configured remote; Yui never
|
|
404
421
|
fetches, rebases, merges, or force-pushes as a hidden side effect of Task
|
|
405
|
-
completion.
|
|
422
|
+
completion. Behind, diverged, or unavailable remote state is reported as
|
|
423
|
+
delivery-risk evidence for the Leader; it does not replace the Leader's choice
|
|
424
|
+
of delivery base. A dirty Task workspace remains a completion blocker.
|
|
406
425
|
|
|
407
426
|
Implementation WorkItems declare the Projects they may modify. Their workspace
|
|
408
427
|
keeps the same relative layout, creates isolated worktrees only for that write
|
|
@@ -704,7 +723,7 @@ yui task complete <task-id> --summary-file delivery.txt \
|
|
|
704
723
|
--accept-published-tree <publication-id>
|
|
705
724
|
```
|
|
706
725
|
|
|
707
|
-
This
|
|
726
|
+
This is an independent exact-tree authorization. Yui requires the current,
|
|
708
727
|
unsuperseded Publication to be merged and verified, its local commit to equal
|
|
709
728
|
the physical Task head, its remote commit to be ancestry-divergent, and both
|
|
710
729
|
commits to resolve to the same exact tree. When a Task-final Review obligation
|
|
@@ -712,10 +731,10 @@ exists, completion also requires the latest semantic Round to attest the
|
|
|
712
731
|
accepted Task head; otherwise completion does not invent a ReviewRound.
|
|
713
732
|
`--refresh-remote` fetches
|
|
714
733
|
the remote object graph before resolving that Publication commit. For a Task
|
|
715
|
-
governed by a durable
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
734
|
+
governed by a durable final-review contract, the stored contract continues to
|
|
735
|
+
require its Reviewer policy, but compatible CLI and Controller updates do not
|
|
736
|
+
need to reproduce its historical control-plane digest. Tasks without that
|
|
737
|
+
contract retain the one-step explicit completion path. The Task event
|
|
719
738
|
audit records the authorization and, on completion, the accepted Project,
|
|
720
739
|
Publication, optional ReviewRound, both commits, and tree.
|
|
721
740
|
|
|
@@ -764,14 +783,22 @@ Global Operator and global Role sessions remain native interactive CLIs:
|
|
|
764
783
|
yui session enter <global-role>
|
|
765
784
|
```
|
|
766
785
|
|
|
786
|
+
An offline Home migration requires a short maintenance window with no managed
|
|
787
|
+
Agent Session running. Once current Turns and Runs have finished, use
|
|
788
|
+
`yui session stop --all` from a normal shell. The command stops Task and global
|
|
789
|
+
Role Sessions only after every one is idle, and leaves all Sessions untouched
|
|
790
|
+
when any Role is still busy. On success it also leaves the Controller stopped;
|
|
791
|
+
run `yui update` next rather than resuming ordinary Task work.
|
|
792
|
+
|
|
767
793
|
tmux fixes a pane's history capacity when that pane is created. Existing panes
|
|
768
794
|
retain their configured capacity; managed runtime output remains observable in
|
|
769
795
|
the Agent Host pane without becoming lifecycle or acknowledgement evidence.
|
|
770
796
|
|
|
771
|
-
Each Role, including a Task-bound Worker instance, can bind multiple
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
ready-to-switch configurations
|
|
797
|
+
Each Role, including Operator and a Task-bound Worker instance, can bind multiple
|
|
798
|
+
configured Agents, has one active Agent, and keeps a separate native session per
|
|
799
|
+
Agent binding. Multiple bindings may use the same adapter for different accounts,
|
|
800
|
+
models, profiles, or environment sources. They are ready-to-switch configurations,
|
|
801
|
+
not parallel writers: the active binding remains the unique authority. Operator can
|
|
775
802
|
keep multiple conversations for each binding. `operator new` and
|
|
776
803
|
`operator resume` reuse the single Operator tmux pane: when a process is
|
|
777
804
|
running, Yui asks before stopping it and switching the conversation. On a
|
|
@@ -813,7 +840,7 @@ movement cannot conceal a workflow that is not advancing.
|
|
|
813
840
|
|
|
814
841
|
Stable Role context is also launch metadata, never a bootstrap turn. Yui passes Role policy and `systemPrompt` through the Agent's native system/developer-instruction channel. Task execution Runs receive the generic Leader or Worker Skill, while review Runs receive the generic Reviewer Skill based on durable Run purpose rather than a configured Role name. These Yui-owned Role Skills define portable orchestration only. Project Skills remain ordinary versioned files in the Project and are discovered, selected, and loaded by the Agent through its native project mechanism; Yui does not scan, parse, copy, or inject them.
|
|
815
842
|
|
|
816
|
-
Native Codex developer instructions carry compact absolute references only for Yui-owned Role Skills, which Codex reads on demand.
|
|
843
|
+
Native Codex developer instructions carry compact absolute references only for Yui-owned Role Skills, which Codex reads on demand. Yui applies this scalar as an invocation-local override, so existing user, profile, project, and system values do not make the Session unusable and the underlying config file is never mutated. A higher-precedence managed `developer_instructions` value remains a bounded launch blocker because Codex will not let invocation flags replace it. Interactive Codex Sessions apply the same rule to Yui's structured `notify` callback; Doctor reports ordinary overridden sources as context and rejects an effective managed conflict. Managed Runs instead use invocation-local Agent Driver Hooks as their sole lifecycle authority and do not claim `notify`. `skills.config` is not misused because it only enables or disables already-discovered Skills. Claude receives the same Yui-owned Role Skill content from a private `0600` managed context file rather than a large or sensitive argv value; retries and resumes reuse the purpose-specific Role path. Non-Operator global Roles stay neutral and receive no Task orchestration Skill. Operator therefore opens at an empty native composer, so the user's text remains its first user message. Leader wakeups and Worker or Reviewer Run assignments remain real mailbox-delivered work messages. An adapter without a native instruction channel must reject this context rather than silently converting it into a first user prompt.
|
|
817
844
|
|
|
818
845
|
## Controller and failure handling
|
|
819
846
|
|
|
@@ -929,6 +956,7 @@ yui config role add|list|show|update|remove|bind|unbind
|
|
|
929
956
|
yui config profile add|list|show|update|remove|reset
|
|
930
957
|
yui config completion [bash|zsh|fish]
|
|
931
958
|
yui session enter|record|replace|reconcile
|
|
959
|
+
yui session stop --all
|
|
932
960
|
yui project add|clone|refresh|update|discover|list|show|knowledge
|
|
933
961
|
```
|
|
934
962
|
|
|
@@ -199,6 +199,12 @@ const globalSessionChildren = [
|
|
|
199
199
|
usage: "yui session replace <role> --native-id <id> --reason <text>",
|
|
200
200
|
options: ["--native-id", "--reason"]
|
|
201
201
|
},
|
|
202
|
+
{
|
|
203
|
+
name: "stop",
|
|
204
|
+
summary: "Stop all idle managed Sessions and the Controller before an offline update.",
|
|
205
|
+
usage: "yui session stop --all",
|
|
206
|
+
options: ["--all"]
|
|
207
|
+
},
|
|
202
208
|
{
|
|
203
209
|
name: "reconcile",
|
|
204
210
|
summary: "Reconcile durable Session owners with native sessions.",
|
|
@@ -1357,14 +1363,16 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1357
1363
|
},
|
|
1358
1364
|
{
|
|
1359
1365
|
name: "session",
|
|
1360
|
-
summary: "
|
|
1366
|
+
summary: "Enter, stop, and reconcile managed Role sessions.",
|
|
1361
1367
|
examples: [
|
|
1362
1368
|
"yui session context operator --json",
|
|
1363
1369
|
"yui session enter operator",
|
|
1370
|
+
"yui session stop --all",
|
|
1364
1371
|
"yui session reconcile --report"
|
|
1365
1372
|
],
|
|
1366
1373
|
sections: [
|
|
1367
1374
|
{ id: "global", title: "Global Role sessions", entries: ["context", "enter", "record", "replace"] },
|
|
1375
|
+
{ id: "maintenance", title: "Maintenance", entries: ["stop"] },
|
|
1368
1376
|
{ id: "recovery", title: "Recovery", entries: ["reconcile"] }
|
|
1369
1377
|
],
|
|
1370
1378
|
children: globalSessionChildren
|
package/dist/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ import { CONFIG_DOMAINS } from "./config/configCatalog.js";
|
|
|
28
28
|
import { runConfigOverview } from "./commands/configOverview.js";
|
|
29
29
|
import { parseControllerCleanupOptions, parseControllerStatusOptions, parseControllerRuntimeSnapshot, renderControllerResourceStatus, renderRuntimeIdentitySection, summarizeDurablePhysicalMismatch, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
|
|
30
30
|
import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
|
|
31
|
-
import { parseSessionReconcileOptions, runSessionReconcileCommand } from "./commands/sessionCommands.js";
|
|
31
|
+
import { parseSessionReconcileOptions, parseSessionStopOptions, runSessionReconcileCommand, runSessionStopCommand } from "./commands/sessionCommands.js";
|
|
32
32
|
import { SessionOwnerReconciliation } from "./controller/sessionOwnerReconciliation.js";
|
|
33
33
|
import { runJobCommand } from "./commands/jobCommands.js";
|
|
34
34
|
import { runDurableJobCommand } from "./commands/durableJobCommands.js";
|
|
@@ -667,6 +667,31 @@ export async function main() {
|
|
|
667
667
|
return;
|
|
668
668
|
}
|
|
669
669
|
if (resolved[0] === "session") {
|
|
670
|
+
if (resolved[1] === "stop") {
|
|
671
|
+
const options = parseSessionStopOptions(resolved.slice(2));
|
|
672
|
+
const result = await runSessionStopCommand({
|
|
673
|
+
options,
|
|
674
|
+
runtime: {
|
|
675
|
+
beginMaintenance: () => acquireHandoverLock(home),
|
|
676
|
+
snapshot: () => ({
|
|
677
|
+
candidates: schedulerStore.listRuntimeSessionCandidates(),
|
|
678
|
+
dormant: schedulerStore.listDormantRuntimeOwners()
|
|
679
|
+
}),
|
|
680
|
+
drainController: () => runtime.drainController(),
|
|
681
|
+
stopController: () => stopFileTaskController(home, {
|
|
682
|
+
environment: process.env
|
|
683
|
+
}),
|
|
684
|
+
startController: async () => {
|
|
685
|
+
await ensureFileTaskController(home, { environment: process.env });
|
|
686
|
+
},
|
|
687
|
+
stopDormantSession: (candidate) => runtime.stopDormantSession(candidate)
|
|
688
|
+
},
|
|
689
|
+
environment: process.env
|
|
690
|
+
});
|
|
691
|
+
process.exitCode = result.exitCode;
|
|
692
|
+
emit(result.output, false, result.data);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
670
695
|
const roleOptions = {
|
|
671
696
|
yuiHome: home,
|
|
672
697
|
env: process.env,
|
|
@@ -198,7 +198,6 @@ function updateRole(args, store, options) {
|
|
|
198
198
|
if (changesAgentConfig) {
|
|
199
199
|
const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
|
|
200
200
|
const agent = requireAgent(agentId, tx);
|
|
201
|
-
assertOperatorAdapterAvailable(role, agent);
|
|
202
201
|
const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
|
|
203
202
|
bindings = { ...role.agentBindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
|
|
204
203
|
}
|
|
@@ -231,7 +230,6 @@ function bindRole(args, store) {
|
|
|
231
230
|
roleName: role.name
|
|
232
231
|
}, "desired Agent binding update");
|
|
233
232
|
const agent = requireAgent(agentId, tx);
|
|
234
|
-
assertOperatorAdapterAvailable(role, agent);
|
|
235
233
|
const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
|
|
236
234
|
const withBinding = updateGlobalRole(role, {
|
|
237
235
|
agentBindings: { ...role.agentBindings, [agentId]: binding }
|
|
@@ -260,16 +258,6 @@ function bindRole(args, store) {
|
|
|
260
258
|
});
|
|
261
259
|
return presentRole(result.message, result.role, store);
|
|
262
260
|
}
|
|
263
|
-
function assertOperatorAdapterAvailable(role, agent) {
|
|
264
|
-
if (role.name !== "operator" || Object.hasOwn(role.agentBindings, agent.id))
|
|
265
|
-
return;
|
|
266
|
-
const existing = Object.values(role.agentBindings).find((binding) => binding.adapterId === agent.adapterId);
|
|
267
|
-
if (existing !== undefined) {
|
|
268
|
-
throw usageError(`Operator already has a ${agent.adapterId} Agent: ${existing.agentId}. `
|
|
269
|
-
+ "Update that Agent's configuration, or activate another adapter and "
|
|
270
|
-
+ "unbind it before binding this Agent.");
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
261
|
function removeRole(args, store) {
|
|
274
262
|
const [rawName, ...rest] = args;
|
|
275
263
|
const name = roleName(rawName);
|
|
@@ -1,5 +1,121 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
2
|
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
3
|
+
export function parseSessionStopOptions(args) {
|
|
4
|
+
if (args.length !== 1 || args[0] !== "--all") {
|
|
5
|
+
throw usageError("Session stop usage: yui session stop --all.");
|
|
6
|
+
}
|
|
7
|
+
return { all: true };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Stop every current managed Session only when all of them are idle. The
|
|
11
|
+
* all-or-nothing preflight prevents a maintenance command from silently
|
|
12
|
+
* stopping half the topology while another Role is still working.
|
|
13
|
+
*/
|
|
14
|
+
export async function runSessionStopCommand(input) {
|
|
15
|
+
void input.options;
|
|
16
|
+
if (input.environment?.YUI_SESSION_SCOPE !== undefined) {
|
|
17
|
+
throw usageError("Session stop must be run by the user from a normal shell, outside a managed Yui Session.");
|
|
18
|
+
}
|
|
19
|
+
const maintenance = input.runtime.beginMaintenance();
|
|
20
|
+
let completed = false;
|
|
21
|
+
try {
|
|
22
|
+
const initial = input.runtime.snapshot();
|
|
23
|
+
const initiallyBlocked = blockedSessions(initial);
|
|
24
|
+
if (initiallyBlocked.length > 0)
|
|
25
|
+
return blockedStopResult(initiallyBlocked);
|
|
26
|
+
// Stop and drain the Controller while the handover fence prevents a new
|
|
27
|
+
// Leader dispatch. The explicit full pass settles pending runtime inbox and
|
|
28
|
+
// lifecycle work that may exist without a current Session candidate.
|
|
29
|
+
await input.runtime.drainController();
|
|
30
|
+
await input.runtime.stopController();
|
|
31
|
+
// Re-read the authoritative facts after the drain because a Run that was
|
|
32
|
+
// already inside a pass may have changed state meanwhile.
|
|
33
|
+
const settled = input.runtime.snapshot();
|
|
34
|
+
const blockedAfterDrain = blockedSessions(settled);
|
|
35
|
+
if (blockedAfterDrain.length > 0) {
|
|
36
|
+
await input.runtime.startController();
|
|
37
|
+
return blockedStopResult(blockedAfterDrain);
|
|
38
|
+
}
|
|
39
|
+
for (const session of settled.candidates) {
|
|
40
|
+
const candidate = settled.dormant.find((entry) => sameStopCandidate(session, entry));
|
|
41
|
+
if (candidate === undefined) {
|
|
42
|
+
throw new Error(`Dormant Session changed during maintenance: ${renderSessionOwner(session)}.`);
|
|
43
|
+
}
|
|
44
|
+
await input.runtime.stopDormantSession(candidate);
|
|
45
|
+
}
|
|
46
|
+
const count = settled.candidates.length;
|
|
47
|
+
completed = true;
|
|
48
|
+
return {
|
|
49
|
+
output: count === 0
|
|
50
|
+
? "No managed Sessions were running. The Controller remains stopped; run `yui update` now.\n"
|
|
51
|
+
: `Stopped ${count} managed Sessions. The Controller remains stopped; run \`yui update\` now.\n`,
|
|
52
|
+
data: { stopped: settled.candidates, blocked: [] },
|
|
53
|
+
exitCode: 0
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!completed) {
|
|
58
|
+
try {
|
|
59
|
+
await input.runtime.startController();
|
|
60
|
+
}
|
|
61
|
+
catch (restartError) {
|
|
62
|
+
throw new AggregateError([error, restartError], "Session maintenance failed and the Controller could not be restarted.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
maintenance.release();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function blockedSessions(input) {
|
|
72
|
+
return input.candidates.flatMap((session) => {
|
|
73
|
+
const idle = input.dormant.some((candidate) => sameStopCandidate(session, candidate));
|
|
74
|
+
if (idle)
|
|
75
|
+
return [];
|
|
76
|
+
return [{
|
|
77
|
+
session,
|
|
78
|
+
reason: session.status === "running"
|
|
79
|
+
? "running"
|
|
80
|
+
: "runtime-work-pending"
|
|
81
|
+
}];
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function blockedStopResult(blocked) {
|
|
85
|
+
const details = blocked.map(({ session, reason }) => (`- ${renderSessionOwner(session)} (${reason === "running"
|
|
86
|
+
? "a Turn or Run is still running"
|
|
87
|
+
: "an active Run or lifecycle operation is still pending"})`));
|
|
88
|
+
return {
|
|
89
|
+
output: [
|
|
90
|
+
`Cannot stop managed Sessions: ${blocked.length} Session(s) are still busy.`,
|
|
91
|
+
...details,
|
|
92
|
+
"No Session was stopped. Let the listed work finish, then run `yui session stop --all` again."
|
|
93
|
+
].join("\n") + "\n",
|
|
94
|
+
data: { stopped: [], blocked },
|
|
95
|
+
exitCode: 5
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function sameStopCandidate(session, candidate) {
|
|
99
|
+
if (session.owner.scope !== candidate.owner.scope)
|
|
100
|
+
return false;
|
|
101
|
+
if (session.owner.roleName !== candidate.owner.roleName)
|
|
102
|
+
return false;
|
|
103
|
+
if (session.owner.scope === "task"
|
|
104
|
+
&& candidate.owner.scope === "task"
|
|
105
|
+
&& session.owner.taskId !== candidate.owner.taskId) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return session.agentId === candidate.agentId
|
|
109
|
+
&& session.adapterId === candidate.adapterId
|
|
110
|
+
&& session.nativeSessionId === candidate.nativeSessionId
|
|
111
|
+
&& session.launchId === candidate.launchId
|
|
112
|
+
&& session.sessionUpdatedAt === candidate.sessionUpdatedAt;
|
|
113
|
+
}
|
|
114
|
+
function renderSessionOwner(session) {
|
|
115
|
+
return session.owner.scope === "task"
|
|
116
|
+
? `${session.owner.taskId}/${session.owner.roleName}`
|
|
117
|
+
: `global/${session.owner.roleName}`;
|
|
118
|
+
}
|
|
3
119
|
export function parseSessionReconcileOptions(args) {
|
|
4
120
|
const allowed = new Set(["--report", "--cleanup"]);
|
|
5
121
|
if (args.some((argument) => !allowed.has(argument))) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { usageError } from "../errors/cliError.js";
|
|
2
|
-
import {
|
|
2
|
+
import { inspectTaskBaseFreshness, renderTaskBaseFreshnessReport } from "../repository/taskBaseFreshness.js";
|
|
3
3
|
export async function runTaskBaseStatusCommand(args, store, options = {}) {
|
|
4
4
|
const usage = "Task base status usage: yui task base status <task> [--refresh].";
|
|
5
5
|
const refresh = args.includes("--refresh");
|
|
@@ -11,19 +11,9 @@ export async function runTaskBaseStatusCommand(args, store, options = {}) {
|
|
|
11
11
|
git: options.git,
|
|
12
12
|
refresh
|
|
13
13
|
});
|
|
14
|
-
assertReportIsDeliverySafe(report);
|
|
15
14
|
return {
|
|
16
15
|
kind: "output",
|
|
17
16
|
output: renderTaskBaseFreshnessReport(report),
|
|
18
17
|
data: report
|
|
19
18
|
};
|
|
20
19
|
}
|
|
21
|
-
function assertReportIsDeliverySafe(report) {
|
|
22
|
-
try {
|
|
23
|
-
assertTaskBaseFreshnessForCompletion(report);
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
-
throw usageError(`${renderTaskBaseFreshnessReport(report)}${message}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -102,7 +102,7 @@ function storedTaskFinalReviewContractResolution(store, taskId) {
|
|
|
102
102
|
* acceptance, and completion mutation must present the same verified
|
|
103
103
|
* capability; shared review-config drift is intentionally irrelevant.
|
|
104
104
|
*/
|
|
105
|
-
function taskFinalReviewContractForMutation(store, taskId, options
|
|
105
|
+
function taskFinalReviewContractForMutation(store, taskId, options) {
|
|
106
106
|
const supplied = options.taskFinalReviewContract;
|
|
107
107
|
if (supplied !== undefined) {
|
|
108
108
|
validateTaskFinalReviewContract(supplied);
|
|
@@ -117,11 +117,12 @@ function taskFinalReviewContractForMutation(store, taskId, options, authorizatio
|
|
|
117
117
|
}
|
|
118
118
|
if (stored === undefined)
|
|
119
119
|
return supplied;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
// Historical exact contracts now express only a durable Reviewer policy.
|
|
121
|
+
// The persisted contract remains the audit authority, but ordinary
|
|
122
|
+
// protocol/storage-compatible CLIs may continue the Task without presenting
|
|
123
|
+
// a release-bound capability on every mutation.
|
|
124
|
+
if (supplied === undefined)
|
|
125
|
+
return stored;
|
|
125
126
|
if (!sameTaskFinalReviewContract(stored, supplied)) {
|
|
126
127
|
throw usageError(`Task final-review contract control-plane digest mismatch for ${taskId}.`);
|
|
127
128
|
}
|
|
@@ -168,18 +169,10 @@ export function preflightTaskCompletion(taskId, store, options = {}, request = {
|
|
|
168
169
|
throw usageError(`Task is archived: ${task.id}.`);
|
|
169
170
|
if (task.status !== "active")
|
|
170
171
|
throw usageError(`Task is not active: ${task.id}.`);
|
|
171
|
-
// Resolve
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
// For the explicit published-tree path only, let that caller authenticate
|
|
176
|
-
// the stored contract far enough to persist an exact authorization fact.
|
|
177
|
-
// The same command must return before any contract-governed completion
|
|
178
|
-
// mutation; the exact Leader later consumes the authorization with the real
|
|
179
|
-
// contract capability.
|
|
180
|
-
const authorizingPublishedTree = request.acceptedPublishedTreePublicationId !== undefined
|
|
181
|
-
&& actor !== "leader";
|
|
182
|
-
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options, { allowStoredWithoutSupplied: authorizingPublishedTree });
|
|
172
|
+
// Resolve the durable Task-local Reviewer policy before any remote fetch or
|
|
173
|
+
// Integration write. Historical control-plane identity is audit evidence,
|
|
174
|
+
// not a capability that every compatible CLI must reproduce.
|
|
175
|
+
const taskFinalReviewContract = taskFinalReviewContractForMutation(store, task.id, options);
|
|
183
176
|
const activeTaskReview = store.listReviewRounds(task.id).some((round) => ((round.scope ?? "work-item") === "task"
|
|
184
177
|
&& (round.status === "pending" || round.status === "running")));
|
|
185
178
|
// Issue 06: one shared readiness projection enumerates every blocker.
|
|
@@ -4375,30 +4368,17 @@ function queueTaskReviewRound(store, task, config, taskCandidate, options, now,
|
|
|
4375
4368
|
* (or a failed round is surfaced as a blocker).
|
|
4376
4369
|
*/
|
|
4377
4370
|
function prepareFinalTaskReview(store, task, now, taskFinalContract, options) {
|
|
4378
|
-
//
|
|
4379
|
-
//
|
|
4380
|
-
//
|
|
4381
|
-
//
|
|
4382
|
-
|
|
4371
|
+
// A Leader-requested ReviewRound is evidence, not policy. Only an explicit
|
|
4372
|
+
// immutable Task contract creates a durable completion obligation; optional
|
|
4373
|
+
// historical Rounds never cause completion to manufacture another Round for
|
|
4374
|
+
// a later head.
|
|
4375
|
+
if (taskFinalContract === undefined || task.projectBindings.length === 0)
|
|
4376
|
+
return null;
|
|
4383
4377
|
const taskRounds = reviewRoundsByIdentity(store.listReviewRounds(task.id))
|
|
4384
4378
|
.filter((round) => ((round.scope ?? "work-item") === "task"
|
|
4385
|
-
&&
|
|
4379
|
+
&& sameTaskFinalReviewContract(round.taskFinalReviewContract, taskFinalContract)));
|
|
4386
4380
|
const establishedRound = taskRounds.at(-1);
|
|
4387
|
-
|
|
4388
|
-
if (taskFinalContract !== undefined) {
|
|
4389
|
-
config = taskFinalReviewConfig(taskFinalContract);
|
|
4390
|
-
}
|
|
4391
|
-
else if (establishedRound === undefined) {
|
|
4392
|
-
// The configured Reviewer is available to the Leader, but does not choose
|
|
4393
|
-
// the Task topology. A managed final Review becomes an obligation only
|
|
4394
|
-
// after the Leader requests one (or an immutable Task contract requires it).
|
|
4395
|
-
config = null;
|
|
4396
|
-
}
|
|
4397
|
-
else {
|
|
4398
|
-
config = { roleName: establishedRound.reviewerRoleName, trigger: "final" };
|
|
4399
|
-
}
|
|
4400
|
-
if (config === null || task.projectBindings.length === 0)
|
|
4401
|
-
return null;
|
|
4381
|
+
const config = taskFinalReviewConfig(taskFinalContract);
|
|
4402
4382
|
const taskCandidate = taskReviewProvenance(store, task, options).candidate;
|
|
4403
4383
|
const latest = establishedRound;
|
|
4404
4384
|
if (latest?.status === "running") {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import { callController, readControllerDiscovery, stopOrphanedFileTaskController, stopPreviousFileTaskController } from "../core/controllerClient.js";
|
|
4
|
+
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
4
5
|
import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
|
|
5
6
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
6
7
|
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
8
|
+
import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
7
9
|
import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
8
10
|
import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
|
|
9
11
|
import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
|
|
@@ -11,6 +13,7 @@ import { yuiVersionIdentity } from "../version.js";
|
|
|
11
13
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
12
14
|
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
13
15
|
import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
16
|
+
import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
|
|
14
17
|
const STARTUP_TIMEOUT_MS = 5_000;
|
|
15
18
|
const POLL_INTERVAL_MS = 50;
|
|
16
19
|
const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
|
|
@@ -502,6 +505,63 @@ export class FileTaskWorkflowRuntime {
|
|
|
502
505
|
throw new Error(`Global Role runtime session is still active: ${roleName}.`);
|
|
503
506
|
}
|
|
504
507
|
}
|
|
508
|
+
/** Drain pending runtime facts while an external maintenance fence is held. */
|
|
509
|
+
async drainController() {
|
|
510
|
+
await ensureFileTaskController(this.home, {
|
|
511
|
+
environment: this.clientOptions.environment
|
|
512
|
+
});
|
|
513
|
+
await callFileTaskController(this.home, "scheduler.scan", {}, {
|
|
514
|
+
...this.clientOptions,
|
|
515
|
+
requestTimeoutMs: LIFECYCLE_REQUEST_TIMEOUT_MS
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Maintenance-only exact stop used after the Controller has fully exited.
|
|
520
|
+
* The dormant candidate fences the durable cleanup request; physical owner
|
|
521
|
+
* records and Task runtime isolation are cleared through the same primitives
|
|
522
|
+
* as the Controller lifecycle path before the Session becomes stopped.
|
|
523
|
+
*/
|
|
524
|
+
async stopDormantSession(candidate) {
|
|
525
|
+
const queuedAt = new Date();
|
|
526
|
+
const target = this.schedulerStore.enqueueRuntimeCleanup(candidate.owner, queuedAt, candidate);
|
|
527
|
+
if (target === null) {
|
|
528
|
+
throw new Error(`Dormant Session changed before maintenance cleanup: ${runtimeOwnerLabel(candidate.owner)}.`);
|
|
529
|
+
}
|
|
530
|
+
const reconciliation = new SessionOwnerReconciliation({
|
|
531
|
+
home: this.home,
|
|
532
|
+
store: this.store,
|
|
533
|
+
environment: this.clientOptions.environment,
|
|
534
|
+
tmux: this.tmux
|
|
535
|
+
});
|
|
536
|
+
const termination = await reconciliation.terminateOwner(candidate.owner);
|
|
537
|
+
if (termination.outcome !== "stop-confirmed") {
|
|
538
|
+
throw new Error(`Role runtime cleanup could not prove physical exit: ${runtimeOwnerLabel(candidate.owner)}; `
|
|
539
|
+
+ termination.remaining
|
|
540
|
+
.map(({ record, detail }) => `${record.launchId}: ${detail}`)
|
|
541
|
+
.join("; "));
|
|
542
|
+
}
|
|
543
|
+
if (candidate.owner.scope === "task" && candidate.launchId !== undefined) {
|
|
544
|
+
const isolation = new FileTaskRuntimeIsolation({
|
|
545
|
+
runtimeRoot: `${this.home}.task-runtimes`,
|
|
546
|
+
controlPlane: {
|
|
547
|
+
yuiHome: this.home,
|
|
548
|
+
controllerSocketPath: controllerSocketPath(this.store.getHomeIdentity().homeId),
|
|
549
|
+
tmuxNamespace: yuiTmuxServerName(this.home),
|
|
550
|
+
globalInstallPaths: [process.execPath]
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
isolation.cleanupTaskLaunch({
|
|
554
|
+
taskId: candidate.owner.taskId,
|
|
555
|
+
launchId: candidate.launchId,
|
|
556
|
+
reason: this.store.getTask(candidate.owner.taskId)?.status === "completed"
|
|
557
|
+
? "completion"
|
|
558
|
+
: "interruption"
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (!this.schedulerStore.completeRuntimeCleanup(target, new Date())) {
|
|
562
|
+
throw new Error(`Role runtime cleanup state changed before completion: ${runtimeOwnerLabel(candidate.owner)}.`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
505
565
|
inspectTaskRolePanes(taskId) {
|
|
506
566
|
return this.tmux.inspectTaskRolePanes(taskId);
|
|
507
567
|
}
|
|
@@ -532,6 +592,11 @@ export class FileTaskWorkflowRuntime {
|
|
|
532
592
|
});
|
|
533
593
|
}
|
|
534
594
|
}
|
|
595
|
+
function runtimeOwnerLabel(owner) {
|
|
596
|
+
return owner.scope === "task"
|
|
597
|
+
? `${owner.taskId}/${owner.roleName}`
|
|
598
|
+
: `global/${owner.roleName}`;
|
|
599
|
+
}
|
|
535
600
|
const MANAGED_RUNTIME_ENVIRONMENT = new Set(YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES);
|
|
536
601
|
function foregroundGlobalRoleEnvironment(store, roleName, source) {
|
|
537
602
|
const role = store.getGlobalRole?.(roleName);
|