@zq-silk/yui 0.12.3 → 0.13.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.
Files changed (32) hide show
  1. package/dist/cli/commandCatalog.js +2 -2
  2. package/dist/cli/updatePorts.js +18 -2
  3. package/dist/cli.js +3 -6
  4. package/dist/commands/durableJobCommands.js +6 -18
  5. package/dist/commands/executionAuditCommands.js +1 -1
  6. package/dist/commands/taskActor.js +35 -61
  7. package/dist/commands/taskCommands.js +260 -200
  8. package/dist/commands/taskIntegrationCommands.js +33 -24
  9. package/dist/commands/taskIntegrationQueueCommands.js +16 -18
  10. package/dist/commands/taskPublicationCommands.js +0 -4
  11. package/dist/commands/taskWorkspaceCommands.js +1 -1
  12. package/dist/context/runContextPack.js +15 -0
  13. package/dist/controller/fileSchedulerStoreAdapter.js +17 -1
  14. package/dist/controller/jobClient.js +5 -8
  15. package/dist/controller/jobControl.js +59 -105
  16. package/dist/executor/workspacePreflightClassification.js +6 -5
  17. package/dist/integration/gitIntegrationService.js +3 -3
  18. package/dist/integration/integrationAttempt.js +13 -9
  19. package/dist/integration/integrationQueueService.js +7 -7
  20. package/dist/lifecycle/exactRunTerminalization.js +146 -6
  21. package/dist/milestone/milestone.js +9 -3
  22. package/dist/review/deltaRecheck.js +1 -1
  23. package/dist/review/reviewFinding.js +3 -3
  24. package/dist/review/reviewRound.js +11 -8
  25. package/dist/run/rejectedYieldAttempt.js +221 -0
  26. package/dist/runtime/tmuxAdapters.js +12 -3
  27. package/dist/scheduler/activeRoleRunDelivery.js +4 -0
  28. package/dist/storage/migration/productionRegistry.js +9 -0
  29. package/dist/storage/taskStore.js +9 -8
  30. package/package.json +1 -1
  31. package/skills/yui-leader/SKILL.md +4 -0
  32. package/skills/yui-operator/SKILL.md +19 -13
@@ -784,8 +784,8 @@ const taskChildren = [
784
784
  {
785
785
  name: "retire",
786
786
  summary: "Retire an incorrect historical Agent Run without deleting its audit record.",
787
- usage: "yui task run retire <task>/<run> --reason <text>",
788
- options: ["--reason"]
787
+ usage: "yui task run retire <task>/<run> --reason <text> [--expected-progress-at <timestamp>] [--agent-id <id>] [--adapter-id <id>] [--native-session-id <id>] [--launch-id <id>]",
788
+ options: ["--reason", "--expected-progress-at", "--progress-at", "--agent-id", "--adapter-id", "--native-session-id", "--launch-id"]
789
789
  }
790
790
  ]
791
791
  },
@@ -765,9 +765,25 @@ function runControllerCommandOutput(home, environment, spawn, method, cliBinary)
765
765
  const args = cliBinary === undefined
766
766
  ? [UPDATE_CLI_PATH, "--json", "controller", method]
767
767
  : ["--json", "controller", method];
768
- const result = spawn(command, args, { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
768
+ const result = spawn(command, args, {
769
+ cwd: process.cwd(),
770
+ env: {
771
+ ...environment,
772
+ YUI_HOME: home,
773
+ // The restart child participates in the handover owned by this parent
774
+ // update process. Without the owner PID it waits on its parent's live
775
+ // lock as though a foreign update owned the Home, then times out.
776
+ YUI_UPDATE_HANDOVER_OWNER_PID: String(process.pid)
777
+ },
778
+ shell: false
779
+ });
769
780
  if (result.error !== undefined || result.status !== 0) {
770
- throw new Error(`Controller ${method} failed (exit ${result.status ?? "null"}).`);
781
+ const detail = structuredErrorMessage(result) ?? result.stderr.toString("utf8").trim();
782
+ const error = new Error(`Controller ${method} failed (exit ${result.status ?? "null"})${detail.length === 0 ? "." : `: ${detail}`}`);
783
+ const code = controllerErrorCodeFromResult(result);
784
+ if (code !== undefined)
785
+ Object.assign(error, { code });
786
+ throw error;
771
787
  }
772
788
  const parsed = JSON.parse(result.stdout.toString("utf8"));
773
789
  if (!isRecord(parsed) || parsed.ok !== true) {
package/dist/cli.js CHANGED
@@ -807,7 +807,7 @@ export async function main() {
807
807
  if (resolved[1] === "integration") {
808
808
  const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
809
809
  environment: process.env,
810
- jobPort: createControllerIntegrationJobPort(home, { environment: process.env, store })
810
+ jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
811
811
  });
812
812
  emit(result.output, false, result.data);
813
813
  return;
@@ -896,10 +896,7 @@ export async function main() {
896
896
  }
897
897
  const reference = cliWorkItemReference(workItemId, process.env);
898
898
  const qualified = `${reference.taskId}/${reference.localId}`;
899
- const actor = taskActor(process.env, reference.taskId);
900
- if (actor === "operator") {
901
- throw usageError("Only the Task Leader may clean a WorkItem from a managed Session.");
902
- }
899
+ taskActor(process.env, reference.taskId);
903
900
  if (disposition === "--runtime-only") {
904
901
  let runtimeCleanup;
905
902
  try {
@@ -1117,7 +1114,7 @@ export async function main() {
1117
1114
  // Keep completion offline by default. An explicit refresh is the only
1118
1115
  // path that may fetch and reconcile a moved remote baseline.
1119
1116
  if (refreshRemote) {
1120
- await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env, store }) });
1117
+ await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env }) });
1121
1118
  }
1122
1119
  }
1123
1120
  }
@@ -1,7 +1,7 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
2
  import { ensureFileTaskController } from "../controller/clientRuntime.js";
3
3
  import { acknowledgeDurableJob, cancelDurableJob, getDurableJob, startDurableJob } from "../controller/jobClient.js";
4
- import { resolveJobCaller, taskActor } from "./taskActor.js";
4
+ import { resolveJobCaller, taskLocalActor } from "./taskActor.js";
5
5
  /**
6
6
  * The textual `--owner` forms accepted by `job start`. The public help text in
7
7
  * the command catalog must document exactly these forms.
@@ -34,10 +34,7 @@ export async function runDurableJobCommand(args, options) {
34
34
  async function startJob(args, options) {
35
35
  const parsed = parseStartArgs(args);
36
36
  await ensureFileTaskController(options.home, { environment: options.environment });
37
- // rr8/rr12: Bind the declared owner to the caller's managed identity. The
38
- // Controller rejects a Reviewer, constrains a Worker to its own Work Item,
39
- // and requires a verified Leader assertion for user scope.
40
- const caller = resolveJobCaller(options.environment, parsed.taskId, options.store);
37
+ const caller = resolveJobCaller(options.environment, parsed.taskId);
41
38
  const params = {
42
39
  taskId: parsed.taskId,
43
40
  owner: parsed.owner,
@@ -89,7 +86,7 @@ async function cancelJob(args, options) {
89
86
  const ref = parseRefArgs(args, "cancel");
90
87
  await ensureFileTaskController(options.home, { environment: options.environment });
91
88
  // rr8/rr12: Bind the cancel request to the caller's managed identity.
92
- const caller = resolveJobCaller(options.environment, ref.taskId, options.store);
89
+ const caller = resolveJobCaller(options.environment, ref.taskId);
93
90
  const result = await cancelDurableJob(options.home, ref.taskId, ref.jobId, caller);
94
91
  if (options.json === true)
95
92
  return `${JSON.stringify(result, null, 2)}\n`;
@@ -99,20 +96,11 @@ async function cancelJob(args, options) {
99
96
  }
100
97
  async function acknowledgeJob(args, options) {
101
98
  const ref = parseRefArgs(args, "acknowledge");
102
- // rr5/f5(a): only the Task Leader may acknowledge an
103
- // unknown-needs-attention job. A non-Leader (Worker, Operator, or plain
104
- // user) is rejected at the CLI boundary.
105
- const actor = taskActor(options.environment, ref.taskId);
106
- if (actor !== "leader") {
107
- throw usageError("Only the Task Leader may acknowledge a DurableJob: "
108
- + `${ref.taskId}.`);
109
- }
110
99
  if (options.store === undefined) {
111
- throw usageError("job acknowledge requires a Task store to resolve the Leader assertion.");
100
+ throw usageError("job acknowledge requires a Task store to resolve the caller.");
112
101
  }
113
- // Carry the full managed task caller, including its ephemeral launch key.
114
- // A durable leaderAssertion by itself is intentionally not sufficient.
115
- const caller = resolveJobCaller(options.environment, ref.taskId, options.store);
102
+ taskLocalActor(options.store, options.environment, ref.taskId, options.home);
103
+ const caller = resolveJobCaller(options.environment, ref.taskId);
116
104
  await ensureFileTaskController(options.home, { environment: options.environment });
117
105
  const result = await acknowledgeDurableJob(options.home, ref.taskId, ref.jobId, caller);
118
106
  if (options.json === true)
@@ -156,7 +156,7 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
156
156
  `Delta-rechecks: ${reviews.deltaRechecks.total} total · `
157
157
  + `${reviews.deltaRechecks.equivalentAndAccepted} accepted · `
158
158
  + `${reviews.deltaRechecks.finding} finding · `
159
- + `${reviews.deltaRechecks.requiresFullReview} requiring Leader decision`
159
+ + `${reviews.deltaRechecks.requiresFullReview} requiring Task Agent decision`
160
160
  ]));
161
161
  }
162
162
  else {
@@ -30,6 +30,24 @@ export function taskActor(environment, taskId) {
30
30
  }
31
31
  return "user";
32
32
  }
33
+ /**
34
+ * Resolve authority for a recoverable Task-local mutation. A managed Leader
35
+ * does not gain that authority from long-lived process environment alone: the
36
+ * command must carry the exact current Turn assertion and it must still match
37
+ * the durable active Run, receipt, Role binding, native Session, and Home.
38
+ * Plain-user and global-Operator behavior remains unchanged.
39
+ */
40
+ export function taskLocalActor(store, environment, taskId, yuiHome) {
41
+ const actor = taskActor(environment, taskId);
42
+ if (actor !== "leader")
43
+ return actor;
44
+ const assertion = leaderActionAssertion(environment ?? {});
45
+ if (assertion === undefined || assertion === "invalid"
46
+ || taskLeaderActionRunId(store, taskId, environment, yuiHome) === undefined) {
47
+ throw usageError(`Task-local Leader authority requires the exact current-Turn assertion: ${taskId}.`);
48
+ }
49
+ return actor;
50
+ }
33
51
  export function projectActor(environment) {
34
52
  const env = environment ?? {};
35
53
  if (env.YUI_SESSION_SCOPE === "task")
@@ -49,29 +67,26 @@ export function projectActor(environment) {
49
67
  }
50
68
  /**
51
69
  * rr8: Resolve the caller identity for a `job.start`/`job.cancel` request from
52
- * the managed Session environment. The Controller binds the declared job owner
53
- * to this identity a Reviewer is rejected outright, a Worker can only touch
54
- * its own Work Item's jobs, and a Leader or plain user retains full access.
70
+ * the managed Session environment. The Controller verifies the Agent Session
71
+ * and its scope; Role does not narrow Task control authority.
55
72
  *
56
73
  * rr12: The identity is now Controller-verified rather than self-reported:
57
74
  * - A managed Task Session (`YUI_SESSION_SCOPE=task`) returns `scope: "task"`
58
- * with its Role and Run. A Leader additionally carries the current in-flight
59
- * Turn receipt (preferring the explicit `YUI_LEADER_ACTION_*` assertion over
60
- * a possibly-stale `YUI_RUN_ID`), which the Controller verifies through the
61
- * same active-Run + in-flight + Session check as `job.acknowledge`.
75
+ * with its Role and Run. For a Leader, the explicit current-Turn Run id is
76
+ * preferred over a possibly stale `YUI_RUN_ID`.
62
77
  *
63
78
  * rr13: A managed Task Session also carries `callerKey` — the
64
79
  * `YUI_JOB_CALLER_KEY` injected at its native Session launch. The Controller
65
80
  * hashes it and compares against the durable `jobCallerKeyHashes` map, so a
66
81
  * client that reads durable state cannot replay the caller. A `user`-scope
67
- * caller is rejected outright for job.start/job.cancel (fail-closed); the
68
- * human operator acts through the Leader Session.
82
+ * caller is rejected outright for job.start/job.cancel (fail-closed); a
83
+ * managed global Agent carries its own durable Session identity.
69
84
  *
70
85
  * A managed Task Session may only start jobs for its own Task. An incomplete
71
86
  * managed identity (role/agent/run/session vars without a scope) is rejected
72
87
  * rather than silently downgraded to user authority.
73
88
  */
74
- export function resolveJobCaller(environment, taskId, store) {
89
+ export function resolveJobCaller(environment, taskId) {
75
90
  const env = environment ?? {};
76
91
  if (env.YUI_SESSION_SCOPE === "task") {
77
92
  if (env.YUI_TASK_ID !== taskId) {
@@ -83,9 +98,7 @@ export function resolveJobCaller(environment, taskId, store) {
83
98
  // Controller boundary.
84
99
  const callerKey = env[JOB_CALLER_KEY_ENV];
85
100
  if (role === LEADER_ROLE) {
86
- // Prefer the explicit current-turn Leader assertion over a possibly
87
- // stale YUI_RUN_ID/launch. The Controller verifies it against the
88
- // active in-flight Leader Run.
101
+ // Prefer the explicit current-Turn Run over a possibly stale YUI_RUN_ID.
89
102
  const assertion = leaderActionAssertion(env);
90
103
  if (assertion !== undefined && assertion !== "invalid") {
91
104
  return {
@@ -93,7 +106,6 @@ export function resolveJobCaller(environment, taskId, store) {
93
106
  taskId,
94
107
  role,
95
108
  runId: assertion.runId,
96
- receiptId: assertion.receiptId,
97
109
  ...(callerKey === undefined ? {} : { callerKey })
98
110
  };
99
111
  }
@@ -107,7 +119,14 @@ export function resolveJobCaller(environment, taskId, store) {
107
119
  };
108
120
  }
109
121
  if (env.YUI_SESSION_SCOPE === "global") {
110
- return userCaller(store, taskId);
122
+ return {
123
+ scope: "global",
124
+ role: env.YUI_ROLE,
125
+ agentId: env.YUI_AGENT_ID,
126
+ adapterId: env.YUI_ADAPTER_ID,
127
+ launchId: env.YUI_LAUNCH_ID,
128
+ nativeSessionId: env.YUI_NATIVE_SESSION_ID
129
+ };
111
130
  }
112
131
  if (env.YUI_ROLE !== undefined
113
132
  || env.YUI_AGENT_ID !== undefined
@@ -115,52 +134,7 @@ export function resolveJobCaller(environment, taskId, store) {
115
134
  || env.YUI_NATIVE_SESSION_ID !== undefined) {
116
135
  throw usageError("Managed Agent identity is incomplete; refusing to infer user authority.");
117
136
  }
118
- return userCaller(store, taskId);
119
- }
120
- /**
121
- * rr12: Build a `scope: "user"` caller. When a store is available, attach the
122
- * active in-flight Leader assertion so the Controller can verify the request
123
- * acts under real Leader authority. Without a store, return the bare
124
- * `{scope: "user"}` which the Controller rejects (fail-closed).
125
- */
126
- function userCaller(store, taskId) {
127
- if (store === undefined)
128
- return { scope: "user" };
129
- const runId = activeLeaderRunId(store, taskId);
130
- if (runId === undefined) {
131
- throw usageError("job.start/job.cancel user scope requires an active in-flight Task Leader: "
132
- + `${taskId}.`);
133
- }
134
- return {
135
- scope: "user",
136
- leaderAssertion: {
137
- runId,
138
- receiptId: formatAgentRunReceiptId(taskId, runId)
139
- }
140
- };
141
- }
142
- /**
143
- * rr12: Resolve the current in-flight Task Leader Run for a non-managed
144
- * (operator/bare-shell) caller. Unlike `taskLeaderActionRunId`, this does not
145
- * require managed Leader environment variables — it verifies the durable
146
- * state directly: an active Leader Run whose Role Session is currently
147
- * in-flight with the matching receipt. Returns undefined when no Leader is
148
- * active or in flight.
149
- */
150
- function activeLeaderRunId(store, taskId) {
151
- const run = store.getActiveAgentRun(taskId, LEADER_ROLE);
152
- if (run === null || run.status !== "active" || run.roleName !== LEADER_ROLE) {
153
- return undefined;
154
- }
155
- const sessions = store.getTaskRoleSessionSet(taskId, LEADER_ROLE);
156
- const expectedReceipt = agentRunDeliveryReceiptId(run);
157
- if (sessions === null
158
- || sessions.inFlight === null
159
- || sessions.inFlight.runId !== run.id
160
- || sessions.inFlight.receiptId !== expectedReceipt) {
161
- return undefined;
162
- }
163
- return run.id;
137
+ return { scope: "user" };
164
138
  }
165
139
  /**
166
140
  * Resolve an exact current Task Leader Run for event attribution.