@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
@@ -6,7 +6,7 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
6
6
  import { createIntegrationAttempt, recordResolutionDecision, supersedeIntegration, updateIntegrationAttempt } from "../integration/integrationAttempt.js";
7
7
  import { GitIntegrationService } from "../integration/gitIntegrationService.js";
8
8
  import { runTaskIntegrationQueueCommand } from "./taskIntegrationQueueCommands.js";
9
- import { taskActor } from "./taskActor.js";
9
+ import { taskLocalActor } from "./taskActor.js";
10
10
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
11
11
  import { runDeliveryGuardPreflight, withGuardWarnings } from "./deliveryGuardPreflight.js";
12
12
  export async function runTaskIntegrationCommand(args, store, home, options = {}) {
@@ -18,12 +18,13 @@ export async function runTaskIntegrationCommand(args, store, home, options = {})
18
18
  return continueIntegration(rest, store, home, now, options);
19
19
  }
20
20
  if (command === "resolve") {
21
- return resolveDecision(rest, store, now, options.environment);
21
+ return resolveDecision(rest, store, now, options.environment, home);
22
22
  }
23
23
  if (command === "abort")
24
- return abortIntegration(rest, store, now(), options);
25
- if (command === "supersede")
26
- return supersedeIntegrationCommand(rest, store, now(), options.environment);
24
+ return abortIntegration(rest, store, now(), options, home);
25
+ if (command === "supersede") {
26
+ return supersedeIntegrationCommand(rest, store, now(), options.environment, home);
27
+ }
27
28
  if (command === "cleanup") {
28
29
  return cleanupIntegration(rest, store, home, options.environment);
29
30
  }
@@ -43,6 +44,7 @@ async function cleanupIntegration(args, store, home, environment) {
43
44
  throw usageError("Task Integration cleanup usage: yui task integration cleanup <task>/<integration>.");
44
45
  }
45
46
  const integration = requireIntegration(store, args[0], environment);
47
+ taskLocalActor(store, environment, integration.taskId, home);
46
48
  if (integration.status !== "committed"
47
49
  && integration.status !== "superseded"
48
50
  && integration.status !== "failed") {
@@ -83,6 +85,7 @@ async function start(args, store, home, now, options) {
83
85
  if (task.status !== "active") {
84
86
  throw usageError(`Task is not active: ${task.id}/${task.status}.`);
85
87
  }
88
+ taskLocalActor(store, options.environment, task.id, home);
86
89
  const changeSetIds = parsed.many.get("--change-set") ?? [];
87
90
  if (changeSetIds.length === 0)
88
91
  throw usageError("--change-set is required.", usage);
@@ -135,6 +138,7 @@ async function start(args, store, home, now, options) {
135
138
  // record creation are atomic, so a concurrent Leader cannot sneak a
136
139
  // duplicate Integration between the guard and the write.
137
140
  const integration = store.transaction((tx) => {
141
+ taskLocalActor(tx, options.environment, task.id, home);
138
142
  const guard = runDeliveryGuardPreflight(tx, task.id, {
139
143
  kind: "integration-start",
140
144
  projectId: project.id,
@@ -165,6 +169,7 @@ async function continueIntegration(args, store, home, now, options) {
165
169
  throw usageError(usage);
166
170
  const integration = requireIntegration(store, parsed.positionals[0], options.environment);
167
171
  requireActiveIntegrationTask(store, integration);
172
+ taskLocalActor(store, options.environment, integration.taskId, home);
168
173
  if (integration.status !== "validating"
169
174
  && integration.status !== "running"
170
175
  && (integration.status !== "blocked"
@@ -178,45 +183,47 @@ async function runIntegration(store, home, integration, now, options) {
178
183
  const output = result.status === "committed"
179
184
  ? `Integrated ${result.attempt.changeSetIds.join(", ")} into ${result.attempt.targetRef} with CAS (${result.attempt.id})\n`
180
185
  : result.status === "blocked"
181
- ? `Integration ${result.attempt.id} requires a Leader resolution decision in ${result.workspace.path}\n`
186
+ ? `Integration ${result.attempt.id} requires a Task Agent resolution decision in ${result.workspace.path}\n`
182
187
  : result.status === "checks-running"
183
188
  ? `Integration ${result.attempt.id} checks are running as DurableJob ${result.job.id}; run 'yui task integration continue ${result.attempt.taskId}/${result.attempt.id}' when the job finishes\n`
184
189
  : `Integration ${result.attempt.id} failed; target ref was not advanced\n`;
185
190
  return { output, data: result };
186
191
  }
187
- function resolveDecision(args, store, now, environment) {
192
+ function resolveDecision(args, store, now, environment, home) {
188
193
  const usage = "Task Integration resolve usage: yui task integration resolve <task>/<integration> --option <manual-resolution|reject> --rationale <text>.";
189
194
  const parsed = parseRepeatable(args, new Set(), new Set(["--option", "--rationale"]), usage);
190
195
  if (parsed.positionals.length !== 1)
191
196
  throw usageError(usage);
192
- const integration = requireIntegration(store, parsed.positionals[0], environment);
193
- const task = requireActiveIntegrationTask(store, integration);
194
- if (taskActor(environment, task.id) !== "leader") {
195
- throw usageError("Only the Task Leader can resolve an Integration conflict.");
196
- }
197
197
  const selectedOption = parsed.one.get("--option");
198
198
  const rationale = parsed.one.get("--rationale");
199
199
  if (selectedOption === undefined || rationale === undefined)
200
200
  throw usageError(usage);
201
- const resolved = recordResolutionDecision(integration, {
202
- action: selectedOption,
203
- rationale
204
- }, now());
205
- store.saveIntegrationAttempt(resolved.taskId, resolved);
201
+ const resolved = store.transaction((tx) => {
202
+ const integration = requireIntegration(tx, parsed.positionals[0], environment);
203
+ const task = requireActiveIntegrationTask(tx, integration);
204
+ const actor = taskLocalActor(tx, environment, task.id, home);
205
+ const updated = recordResolutionDecision(integration, {
206
+ action: selectedOption,
207
+ rationale
208
+ }, actor, now());
209
+ tx.saveIntegrationAttempt(updated.taskId, updated);
210
+ return updated;
211
+ });
206
212
  return {
207
213
  output: selectedOption === "manual-resolution"
208
- ? `Leader selected manual resolution for ${resolved.id}; resolve its candidate worktree, then run integration continue\n`
209
- : `Leader rejected Integration ${resolved.id}\n`,
214
+ ? `Selected manual resolution for ${resolved.id}; resolve its candidate worktree, then run integration continue\n`
215
+ : `Rejected Integration ${resolved.id}\n`,
210
216
  data: { integration: resolved }
211
217
  };
212
218
  }
213
- async function abortIntegration(args, store, now, options) {
219
+ async function abortIntegration(args, store, now, options, home) {
214
220
  const usage = "Task Integration abort usage: yui task integration abort <task>/<integration> --reason <text>.";
215
221
  const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
216
222
  if (parsed.positionals.length !== 1)
217
223
  throw usageError(usage);
218
224
  const integration = requireIntegration(store, parsed.positionals[0], options.environment);
219
225
  requireActiveIntegrationTask(store, integration);
226
+ taskLocalActor(store, options.environment, integration.taskId, home);
220
227
  if (integration.status !== "running" && integration.status !== "blocked") {
221
228
  throw usageError(`Integration cannot be aborted from ${integration.status}: ${integration.id}.`);
222
229
  }
@@ -238,6 +245,7 @@ async function abortIntegration(args, store, now, options) {
238
245
  if (current.status !== "running" && current.status !== "blocked") {
239
246
  throw usageError(`Integration cannot be aborted from ${current.status}: ${current.id}.`);
240
247
  }
248
+ taskLocalActor(tx, options.environment, current.taskId, home);
241
249
  const aborted = updateIntegrationAttempt(current, {
242
250
  status: "failed",
243
251
  checks: [
@@ -252,7 +260,7 @@ async function abortIntegration(args, store, now, options) {
252
260
  };
253
261
  });
254
262
  }
255
- function supersedeIntegrationCommand(args, store, now, environment) {
263
+ function supersedeIntegrationCommand(args, store, now, environment, home) {
256
264
  const usage = "Task Integration supersede usage: yui task integration supersede <task>/<integration> --reason <text>.";
257
265
  const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
258
266
  if (parsed.positionals.length !== 1)
@@ -265,9 +273,9 @@ function supersedeIntegrationCommand(args, store, now, environment) {
265
273
  const reason = parsed.one.get("--reason");
266
274
  if (reason === undefined)
267
275
  throw usageError(usage);
268
- // Only the Task Leader (or Operator/user) may supersede a committed
269
- // Integration: it rewrites delivery-baseline evidence and audit history.
270
- taskActor(environment, integration.taskId);
276
+ // Superseding a committed Integration rewrites delivery-baseline evidence
277
+ // and audit history, so it remains an explicit Task-control decision.
278
+ taskLocalActor(store, environment, integration.taskId, home);
271
279
  // A queue-backed committed Attempt cannot be superseded: the queue entry
272
280
  // would remain in its current status while its Attempt becomes "superseded",
273
281
  // leaving contradictory terminal records that never converge. This covers
@@ -287,6 +295,7 @@ function supersedeIntegrationCommand(args, store, now, environment) {
287
295
  if (current.status !== "committed") {
288
296
  throw usageError(`Integration cannot be superseded from ${current.status}: ${current.id}.`);
289
297
  }
298
+ taskLocalActor(tx, environment, current.taskId, home);
290
299
  const superseded = supersedeIntegration(current, reason, now);
291
300
  tx.saveIntegrationAttempt(superseded.taskId, superseded);
292
301
  return {
@@ -3,7 +3,7 @@ import { defaultTableWidth, renderTable } from "../output/table.js";
3
3
  import { resolveProject } from "../repository/project.js";
4
4
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
5
5
  import { enqueueIntegrationQueueEntry, processIntegrationQueue, reconcileIntegrationQueueEntry, requeueIntegrationQueueEntry, supersedeIntegrationQueueEntry } from "../integration/integrationQueueService.js";
6
- import { taskActor } from "./taskActor.js";
6
+ import { taskLocalActor } from "./taskActor.js";
7
7
  import { parseRepeatable } from "./taskIntegrationCommands.js";
8
8
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
9
9
  /**
@@ -22,11 +22,11 @@ export async function runTaskIntegrationQueueCommand(args, store, home, options
22
22
  if (command === "process")
23
23
  return process(rest, store, home, options);
24
24
  if (command === "supersede")
25
- return supersede(rest, store, options);
25
+ return supersede(rest, store, options, home);
26
26
  if (command === "requeue")
27
- return requeue(rest, store, options);
27
+ return requeue(rest, store, options, home);
28
28
  if (command === "reconcile")
29
- return reconcile(rest, store, options);
29
+ return reconcile(rest, store, options, home);
30
30
  throw usageError(command === undefined
31
31
  ? "Task Integration queue command is required."
32
32
  : `Unknown command: task integration queue ${command}`);
@@ -37,7 +37,7 @@ async function enqueue(args, store, options, home) {
37
37
  if (parsed.positionals.length !== 1)
38
38
  throw usageError(usage);
39
39
  const task = requireActiveTask(store, parsed.positionals[0]);
40
- requireLeader(options.environment, task.id);
40
+ requireTaskControlActor(store, options.environment, task.id, home);
41
41
  const projectRef = parsed.one.get("--project");
42
42
  const changeSetId = parsed.one.get("--change-set");
43
43
  if (projectRef === undefined || changeSetId === undefined)
@@ -120,7 +120,7 @@ async function process(args, store, home, options) {
120
120
  if (parsed.positionals.length !== 1)
121
121
  throw usageError(usage);
122
122
  const task = requireActiveTask(store, parsed.positionals[0]);
123
- requireLeader(options.environment, task.id);
123
+ requireTaskControlActor(store, options.environment, task.id, home);
124
124
  const limitValue = parsed.one.get("--limit");
125
125
  const limit = limitValue === undefined ? undefined : Number.parseInt(limitValue, 10);
126
126
  if (limitValue !== undefined && (!Number.isSafeInteger(limit) || limit < 1)) {
@@ -148,40 +148,40 @@ async function process(args, store, home, options) {
148
148
  : `${lines.join("\n")}\n`;
149
149
  return { output, data: { processed } };
150
150
  }
151
- function supersede(args, store, options) {
151
+ function supersede(args, store, options, home) {
152
152
  const usage = "Task Integration queue supersede usage: yui task integration queue supersede <task>/<entry> --reason <text>.";
153
153
  const parsed = parseRepeatable(args, new Set(), new Set(["--reason"]), usage);
154
154
  if (parsed.positionals.length !== 1)
155
155
  throw usageError(usage);
156
156
  const entry = requireQueueEntry(store, parsed.positionals[0], options.environment);
157
- requireLeader(options.environment, entry.taskId);
157
+ const actor = requireTaskControlActor(store, options.environment, entry.taskId, home);
158
158
  const reason = parsed.one.get("--reason");
159
159
  if (reason === undefined)
160
160
  throw usageError(usage);
161
- const superseded = supersedeIntegrationQueueEntry(store, entry.taskId, entry.id, reason, options.now ?? (() => new Date()));
161
+ const superseded = supersedeIntegrationQueueEntry(store, entry.taskId, entry.id, reason, actor, options.now ?? (() => new Date()));
162
162
  return {
163
163
  output: `Superseded ${superseded.id}: ${reason}\n`,
164
164
  data: { entry: superseded }
165
165
  };
166
166
  }
167
- function requeue(args, store, options) {
167
+ function requeue(args, store, options, home) {
168
168
  const usage = "Task Integration queue requeue usage: yui task integration queue requeue <task>/<entry>.";
169
169
  if (args.length !== 1)
170
170
  throw usageError(usage);
171
171
  const entry = requireQueueEntry(store, args[0], options.environment);
172
- requireLeader(options.environment, entry.taskId);
173
- const waiting = requeueIntegrationQueueEntry(store, entry.taskId, entry.id, options.now ?? (() => new Date()));
172
+ const actor = requireTaskControlActor(store, options.environment, entry.taskId, home);
173
+ const waiting = requeueIntegrationQueueEntry(store, entry.taskId, entry.id, actor, options.now ?? (() => new Date()));
174
174
  return {
175
175
  output: `Requeued ${waiting.id}; it will be processed again on the next queue run\n`,
176
176
  data: { entry: waiting }
177
177
  };
178
178
  }
179
- async function reconcile(args, store, options) {
179
+ async function reconcile(args, store, options, home) {
180
180
  const usage = "Task Integration queue reconcile usage: yui task integration queue reconcile <task>/<entry>.";
181
181
  if (args.length !== 1)
182
182
  throw usageError(usage);
183
183
  const entry = requireQueueEntry(store, args[0], options.environment);
184
- requireLeader(options.environment, entry.taskId);
184
+ requireTaskControlActor(store, options.environment, entry.taskId, home);
185
185
  const committed = await reconcileIntegrationQueueEntry(store, entry.taskId, entry.id, new NodeGitWorkspace(), options.now ?? (() => new Date()));
186
186
  return {
187
187
  output: `Reconciled ${committed.id} as committed -> ${committed.targetAfter ?? "-"}\n`,
@@ -221,8 +221,6 @@ function requireQueueEntry(store, value, environment) {
221
221
  }
222
222
  return entry;
223
223
  }
224
- function requireLeader(environment, taskId) {
225
- if (taskActor(environment, taskId) !== "leader") {
226
- throw usageError("Only the Task Leader can change the integration queue.");
227
- }
224
+ function requireTaskControlActor(store, environment, taskId, home) {
225
+ return taskLocalActor(store, environment, taskId, home);
228
226
  }
@@ -34,10 +34,6 @@ function addPublication(args, store, options) {
34
34
  throw usageError(`Project is not bound to Task ${task.id}: ${projectReference}.`, usage);
35
35
  }
36
36
  const actor = taskActor(options.environment, task.id);
37
- if (actor === "leader") {
38
- throw usageError("A managed Task Leader cannot record external publication evidence; "
39
- + "use a user or global Operator session.", usage);
40
- }
41
37
  const state = enumOption(parsed.options, "--state", STATES, usage) ?? "open";
42
38
  const verification = parseVerification(parsed, usage);
43
39
  const input = {
@@ -135,7 +135,7 @@ function replaceTaskCommand(args, store, options) {
135
135
  }
136
136
  const now = options.now?.() ?? new Date();
137
137
  const milestone = createMilestone(store.nextMilestoneId(replacement.id), replacement.id, `Replaces ${old.id}`, `Replacement Task for ${old.id} (${old.status}); the original Task, its refs, `
138
- + "and its evidence are preserved and were not rewritten.", now);
138
+ + "and its evidence are preserved and were not rewritten.", "user", now);
139
139
  store.saveMilestone(replacement.id, milestone);
140
140
  return {
141
141
  output: `${created.output}\nRecorded replacement of ${old.id} on ${replacement.id} `
@@ -1,3 +1,4 @@
1
+ import { rejectedYieldAttemptFromTaskEvent, RUN_YIELD_REJECTED_EVENT } from "../run/rejectedYieldAttempt.js";
1
2
  import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
2
3
  import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
3
4
  import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
@@ -426,6 +427,20 @@ function collectAuthorizedContext(store, run) {
426
427
  for (const event of publishedTreeAuthorizations.reverse().slice(-16)) {
427
428
  result.push(materialize("L4", "task-event", event.id, event));
428
429
  }
430
+ for (const event of events.filter(({ type }) => (type === RUN_YIELD_REJECTED_EVENT)).slice(-16)) {
431
+ result.push(materialize("L4", "task-event", event.id, event));
432
+ const attempt = rejectedYieldAttemptFromTaskEvent(event);
433
+ if (attempt === null)
434
+ continue;
435
+ const rejectedRun = store.getAgentRun(task.id, attempt.runId);
436
+ if (rejectedRun !== null) {
437
+ result.push(materialize("L4", "agent-run", rejectedRun.id, rejectedRun));
438
+ }
439
+ const rejectedRound = store.getReviewRound(task.id, attempt.reviewRoundId);
440
+ if (rejectedRound !== null) {
441
+ result.push(materialize("L3", "review-round", rejectedRound.id, rejectedRound));
442
+ }
443
+ }
429
444
  for (const request of store.listOpenInputRequests([task.id])) {
430
445
  result.push(materialize("L4", "input-request", request.id, request));
431
446
  }
@@ -1714,8 +1714,24 @@ export class FileSchedulerStoreAdapter {
1714
1714
  }
1715
1715
  }
1716
1716
  }
1717
- store.saveRole(task.id, updateRoleStatus(role, "exited", input.now));
1717
+ const blocksAutomaticLeaderRecovery = role.name === "leader"
1718
+ && input.leaderRecovery === "blocked";
1719
+ store.saveRole(task.id, updateRoleStatus(role, blocksAutomaticLeaderRecovery ? "failed" : "exited", input.now));
1718
1720
  stopTaskSessionIfPresent(store, task.id, role.name, currentRun.effective.agentId, input.now);
1721
+ if (blocksAutomaticLeaderRecovery) {
1722
+ const failure = recordLeaderFailure(task.id, input.session?.nativeSessionId ?? "(unregistered)", input.summary, input.now, store.getLeaderFailure(task.id));
1723
+ store.saveLeaderFailure(failure);
1724
+ recordLeaderAttentionRequired(store, {
1725
+ taskId: task.id,
1726
+ reason: "leader-recovery-failed",
1727
+ payload: {
1728
+ message: failure.message,
1729
+ runId: currentRun.id
1730
+ },
1731
+ now: input.now
1732
+ });
1733
+ return "failed";
1734
+ }
1719
1735
  queueLeaderWakeup(store, task.id, wakeReason(role.name === "leader" ? "leader-run-failed" : "role-run-failed"), input.now);
1720
1736
  return "failed";
1721
1737
  });
@@ -68,16 +68,13 @@ function parseJobResult(value) {
68
68
  * list/show paths do not use this port and stay lean.
69
69
  */
70
70
  export function createControllerIntegrationJobPort(home, clientOptions = {}) {
71
- const { store, ...controllerOptions } = clientOptions;
72
71
  return {
73
72
  async startCheckJob(input) {
74
73
  const owner = {
75
74
  kind: "integration-attempt",
76
75
  integrationAttemptId: input.integrationId
77
76
  };
78
- // rr8/rr12: Bind the Integration-issued job to the caller's managed
79
- // identity. A user-scope caller carries a verified Leader assertion.
80
- const caller = resolveJobCaller(controllerOptions.environment, input.taskId, store);
77
+ const caller = resolveJobCaller(clientOptions.environment, input.taskId);
81
78
  const { job } = await startDurableJob(home, {
82
79
  taskId: input.taskId,
83
80
  owner,
@@ -87,16 +84,16 @@ export function createControllerIntegrationJobPort(home, clientOptions = {}) {
87
84
  env: input.env,
88
85
  steps: input.steps,
89
86
  caller
90
- }, controllerOptions);
87
+ }, clientOptions);
91
88
  return job;
92
89
  },
93
90
  async getJob(taskId, jobId) {
94
- return getDurableJob(home, taskId, jobId, controllerOptions);
91
+ return getDurableJob(home, taskId, jobId, clientOptions);
95
92
  },
96
93
  async cancelJob(taskId, jobId) {
97
94
  // rr8/rr12: Bind the cancel request to the caller's managed identity.
98
- const caller = resolveJobCaller(controllerOptions.environment, taskId, store);
99
- await cancelDurableJob(home, taskId, jobId, caller, controllerOptions);
95
+ const caller = resolveJobCaller(clientOptions.environment, taskId);
96
+ await cancelDurableJob(home, taskId, jobId, caller, clientOptions);
100
97
  }
101
98
  };
102
99
  }