@zq-silk/yui 2.8.1 → 2.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.
Files changed (119) hide show
  1. package/README.md +5 -3
  2. package/dist/cli/commandCatalog.js +56 -21
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli/interactionPolicy.js +2 -2
  5. package/dist/commands/durableJobCommands.js +50 -0
  6. package/dist/commands/globalRoleCommands.js +30 -9
  7. package/dist/commands/inboxCommands.js +79 -22
  8. package/dist/commands/projectCommands.js +4 -33
  9. package/dist/commands/scheduleCommands.js +45 -0
  10. package/dist/commands/taskActivationCommands.js +19 -5
  11. package/dist/commands/taskCommands.js +8 -2
  12. package/dist/commands/taskFactCommands.js +24 -12
  13. package/dist/commands/taskProjectDeliveryCommand.js +56 -0
  14. package/dist/commands/taskPublicationCommands.js +6 -3
  15. package/dist/commands/taskPublicationVerifyCommand.js +74 -28
  16. package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
  17. package/dist/context/runContextPack.js +25 -4
  18. package/dist/context/runInputContract.js +1 -0
  19. package/dist/context/taskContext.js +53 -16
  20. package/dist/context/taskPhase.js +99 -0
  21. package/dist/controlPlaneCli.js +38 -17
  22. package/dist/controller/controller.js +35 -0
  23. package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
  24. package/dist/controller/globalInputDelivery.js +17 -7
  25. package/dist/controller/inboxTurnCheck.js +64 -0
  26. package/dist/controller/jobControl.js +42 -12
  27. package/dist/controller/jobDefinitionControl.js +304 -0
  28. package/dist/controller/jobSupervisor.js +47 -32
  29. package/dist/controller/providerRetryAdmission.js +2 -0
  30. package/dist/controller/runtime.js +11 -2
  31. package/dist/controller/scheduleControl.js +176 -0
  32. package/dist/coordination/workMailbox.js +3 -3
  33. package/dist/external/githubPublicationVerifier.js +14 -5
  34. package/dist/external/gitlabPublicationVerifier.js +12 -29
  35. package/dist/grant/taskAuthorization.js +7 -2
  36. package/dist/interaction/notificationPresentation.js +12 -3
  37. package/dist/interaction/operatorPresentation.js +5 -0
  38. package/dist/job/builtinJob.js +58 -0
  39. package/dist/job/durableJob.js +72 -49
  40. package/dist/job/jobDefinition.js +68 -0
  41. package/dist/job/jobRunner.js +2 -1
  42. package/dist/job/projectRefreshRunner.js +48 -0
  43. package/dist/message/globalInboxRecovery.js +44 -0
  44. package/dist/message/globalInterrupt.js +1 -1
  45. package/dist/message/inboxReceipt.js +14 -6
  46. package/dist/message/inboxTurn.js +86 -0
  47. package/dist/message/message.js +11 -0
  48. package/dist/message/taskInboxOwnership.js +20 -6
  49. package/dist/repository/projectRefresh.js +58 -0
  50. package/dist/repository/taskWorkspacePreparer.js +2 -1
  51. package/dist/repository/workspaceCleanupInspection.js +6 -1
  52. package/dist/schedule/schedule.js +108 -0
  53. package/dist/scheduler/operatorEvent.js +2 -0
  54. package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
  55. package/dist/storage/contextRecords.js +8 -0
  56. package/dist/storage/jobSchema.js +15 -0
  57. package/dist/storage/maintenanceCaller.js +1 -1
  58. package/dist/storage/recordValidation.js +5 -0
  59. package/dist/storage/scheduleSchema.js +12 -0
  60. package/dist/storage/sqliteSchema.js +95 -24
  61. package/dist/storage/sqliteStore.js +123 -10
  62. package/dist/storage/storageVersions.js +1 -1
  63. package/dist/storage/storeRpc.js +5 -0
  64. package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
  65. package/dist/task/archiveDiagnostics.js +1 -1
  66. package/dist/task/archivePreflight.js +1 -1
  67. package/dist/task/projectDeliveryScope.js +20 -0
  68. package/dist/task/publicationToolObservation.js +126 -0
  69. package/dist/task/remoteDelivery.js +19 -8
  70. package/dist/task/remoteDeliveryService.js +5 -4
  71. package/dist/task/taskActivationService.js +35 -0
  72. package/dist/web/assets/client/app/settings.js +7 -1
  73. package/dist/web/assets/client/i18n/en.js +65 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +65 -2
  75. package/dist/web/assets/client/index.js +2 -0
  76. package/dist/web/assets/client/views/jobs.js +222 -0
  77. package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
  78. package/dist/web/assets/client/views/task/page.js +11 -1
  79. package/dist/web/assets/shared/manageNav.js +1 -0
  80. package/dist/web/assets/shared/taskTabs.js +2 -2
  81. package/dist/web/assets/shell/settingsContent.js +3 -0
  82. package/dist/web/assets/styles/index.js +2 -0
  83. package/dist/web/assets/styles/views/jobs.js +19 -0
  84. package/dist/web/controllerWeb.js +73 -21
  85. package/dist/web/taskPreviews.js +21 -13
  86. package/dist/web/webAccess.js +109 -0
  87. package/dist/web/webJobs.js +52 -0
  88. package/dist/web/webServer.js +76 -44
  89. package/docs/architecture/capabilities-and-resources.md +6 -4
  90. package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
  91. package/docs/cli-information-contract.md +57 -4
  92. package/docs/cli-information-contract.zh-CN.md +41 -2
  93. package/docs/job-runs.md +224 -0
  94. package/docs/project-delivery-scope.md +59 -0
  95. package/docs/release-workflow.md +86 -0
  96. package/docs/release-workflow.zh-CN.md +68 -0
  97. package/docs/schedules.md +132 -0
  98. package/docs/storage-baseline.md +31 -1
  99. package/docs/storage-baseline.zh-CN.md +25 -1
  100. package/docs/task-delivery.md +74 -2
  101. package/docs/task-delivery.zh-CN.md +39 -1
  102. package/docs/testing/verification-levels.md +6 -2
  103. package/docs/testing/verification-levels.zh-CN.md +6 -3
  104. package/i18n/README.zh-CN.md +6 -3
  105. package/package.json +4 -3
  106. package/skills/yui-leader/SKILL.md +44 -1
  107. package/skills/yui-leader/VERSION +1 -1
  108. package/skills/yui-leader/references/planning.md +9 -1
  109. package/skills/yui-operator/SKILL.md +31 -0
  110. package/skills/yui-operator/VERSION +1 -1
  111. package/skills/yui-reviewer/SKILL.md +4 -0
  112. package/skills/yui-reviewer/VERSION +1 -1
  113. package/skills/yui-runtime/SKILL.md +67 -8
  114. package/skills/yui-runtime/VERSION +1 -1
  115. package/skills/yui-runtime/references/context-ordering.md +103 -0
  116. package/skills/yui-runtime/references/publication.md +44 -4
  117. package/skills/yui-runtime/references/recovery.md +21 -0
  118. package/skills/yui-worker/SKILL.md +5 -0
  119. package/skills/yui-worker/VERSION +1 -1
@@ -4,8 +4,15 @@ import { upsertTaskPublication } from "./taskPublicationCommands.js";
4
4
  import { dataError, runtimeError, taskNotFound, usageError, CliError } from "../errors/cliError.js";
5
5
  import { publicationExternalKey } from "../task/publicationReference.js";
6
6
  import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
7
+ import { assertPublicationToolObservation, canonicalPublicationUrl, parsePublicationToolObservation, publicationObservationNotBefore } from "../task/publicationToolObservation.js";
7
8
  export async function runTaskPublicationVerifyCommand(args, store, options) {
8
- const reference = parsePublicationVerificationReference(args);
9
+ const observationIndex = args.indexOf("--observation");
10
+ if (observationIndex !== -1 && observationIndex !== args.length - 2) {
11
+ throw usageError("--observation must be followed by one JSON object at the end of the command.");
12
+ }
13
+ const supplied = observationIndex === -1 ? undefined
14
+ : parsePublicationToolObservation(args[observationIndex + 1]);
15
+ const reference = parsePublicationVerificationReference(observationIndex === -1 ? args : args.slice(0, observationIndex));
9
16
  const task = store.getTask(reference.taskId);
10
17
  if (task === null)
11
18
  throw taskNotFound(reference.taskId);
@@ -13,31 +20,48 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
13
20
  throw usageError(`Archived Task Publication cannot be verified: ${task.id}.`);
14
21
  }
15
22
  taskActor(options.environment, task.id);
16
- const request = preparePublicationVerification(store, reference.taskId, reference.localId, options.environment);
17
- const verifier = options.verifiers[request.publication.provider];
18
- if (verifier === undefined) {
19
- throw usageError(`Publication verification is not supported for provider `
20
- + `${request.publication.provider}.`);
21
- }
23
+ const request = preparePublicationVerification(store, reference.taskId, reference.localId, options.environment, supplied);
24
+ const now = options.now?.() ?? new Date();
22
25
  let observation;
23
- try {
24
- observation = await verifier.inspect({
25
- provider: request.publication.provider,
26
- repository: request.publication.repository,
27
- externalKind: request.publication.externalKind,
28
- externalId: request.publication.externalId,
29
- ...(request.publication.externalUrl === undefined
30
- ? {}
31
- : { externalUrl: request.publication.externalUrl }),
32
- expectedLocalCommit: request.expectedLocalCommit
33
- });
34
- }
35
- catch (error) {
36
- if (error instanceof CliError)
37
- throw error;
38
- throw runtimeError(`Publication verification failed for ${request.publication.provider}/`
39
- + `${request.publication.repository}/${request.publication.externalId}.`, { cause: error });
26
+ if (supplied !== undefined) {
27
+ if (request.publication.evidence === supplied.evidence
28
+ && request.publication.supersedes === supplied.publicationId) {
29
+ return {
30
+ kind: "output",
31
+ output: `Tool observation already recorded: ${request.publication.id} (${request.publication.verification}).\n`,
32
+ data: { publication: request.publication, observation: supplied }
33
+ };
34
+ }
35
+ assertPublicationToolObservation(request.publication, supplied, now, request.observationNotBefore);
36
+ observation = supplied;
40
37
  }
38
+ else
39
+ try {
40
+ const verifier = options.verifiers[request.publication.provider];
41
+ if (verifier === undefined) {
42
+ throw usageError(`No built-in verifier for ${request.publication.provider}; `
43
+ + "use --observation with evidence from an authorized tool.");
44
+ }
45
+ observation = await verifier.inspect({
46
+ provider: request.publication.provider,
47
+ repository: request.publication.repository,
48
+ externalKind: request.publication.externalKind,
49
+ externalId: request.publication.externalId,
50
+ ...(request.publication.externalUrl === undefined
51
+ ? {}
52
+ : { externalUrl: request.publication.externalUrl }),
53
+ ...(request.publication.targetBranch === undefined
54
+ ? {}
55
+ : { targetBranch: request.publication.targetBranch }),
56
+ expectedLocalCommit: request.expectedLocalCommit
57
+ });
58
+ }
59
+ catch (error) {
60
+ if (error instanceof CliError)
61
+ throw error;
62
+ throw runtimeError(`Publication verification failed for ${request.publication.provider}/`
63
+ + `${request.publication.repository}/${request.publication.externalId}.`, { cause: error });
64
+ }
41
65
  assertVerificationObservation(request, observation);
42
66
  const result = commitPublicationVerification(store, request, observation, options.now?.() ?? new Date());
43
67
  return {
@@ -54,7 +78,7 @@ export async function runTaskPublicationVerifyCommand(args, store, options) {
54
78
  data: { publication: result.reference, observation }
55
79
  };
56
80
  }
57
- function preparePublicationVerification(store, taskId, publicationId, environment) {
81
+ function preparePublicationVerification(store, taskId, publicationId, environment, supplied) {
58
82
  return store.transaction((reader) => {
59
83
  const task = reader.getTask(taskId);
60
84
  if (task === null)
@@ -63,11 +87,18 @@ function preparePublicationVerification(store, taskId, publicationId, environmen
63
87
  throw usageError(`Archived Task Publication cannot be verified: ${task.id}.`);
64
88
  }
65
89
  const actor = taskActor(environment, task.id);
66
- const publication = reader.getPublicationReference(task.id, publicationId);
90
+ let publication = reader.getPublicationReference(task.id, publicationId);
67
91
  if (publication === null) {
68
92
  throw dataError(`Publication reference not found: ${task.id}/${publicationId}.`);
69
93
  }
70
94
  const current = reader.findPublicationReferenceByExternalKey(publicationExternalKey(publication));
95
+ // Only the immediate immutable successor of this exact observation is a
96
+ // retry. A later observation/candidate must never revive an old receipt.
97
+ if (current?.taskId === task.id && current.supersedes === publication.id
98
+ && supplied?.taskId === task.id && supplied.publicationId === publication.id
99
+ && current.evidence === supplied.evidence) {
100
+ publication = current;
101
+ }
71
102
  if (current === null || current.taskId !== task.id || current.id !== publication.id) {
72
103
  throw usageError(`Publication ${publication.id} is not the current unsuperseded record `
73
104
  + `for ${publicationExternalKey(publication)}.`);
@@ -84,7 +115,9 @@ function preparePublicationVerification(store, taskId, publicationId, environmen
84
115
  task: taskVerificationInvariant(task),
85
116
  actor,
86
117
  publication,
87
- expectedLocalCommit: publication.localCommit
118
+ expectedLocalCommit: publication.localCommit,
119
+ observationNotBefore: supplied === undefined ? undefined
120
+ : publicationObservationNotBefore(publication, id => reader.getPublicationReference(task.id, id))
88
121
  };
89
122
  });
90
123
  }
@@ -99,6 +132,18 @@ function assertVerificationObservation(request, observation) {
99
132
  if (!/^[0-9a-f]{40}$/u.test(observation.headCommit)) {
100
133
  throw usageError("Provider observation requires an exact remote head.");
101
134
  }
135
+ if (publication.externalUrl !== undefined
136
+ && (observation.externalUrl === undefined
137
+ || canonicalPublicationUrl(observation.externalUrl, publication.provider)
138
+ !== canonicalPublicationUrl(publication.externalUrl, publication.provider))) {
139
+ throw usageError("Provider returned mismatched Publication service or URL.");
140
+ }
141
+ if (observation.targetBranch === undefined || observation.targetBranch.trim().length === 0) {
142
+ throw usageError("Provider observation requires an exact target branch.");
143
+ }
144
+ if (publication.targetBranch !== undefined && observation.targetBranch !== publication.targetBranch) {
145
+ throw usageError("Provider returned mismatched Publication target branch.");
146
+ }
102
147
  if (observation.state === "merged" && observation.remoteCommit === undefined) {
103
148
  throw usageError(`Merged remote ${publication.externalKind} did not expose a remote commit.`);
104
149
  }
@@ -130,6 +175,7 @@ function commitPublicationVerification(store, request, observation, now) {
130
175
  ...(observation.externalUrl === undefined
131
176
  ? {}
132
177
  : { externalUrl: observation.externalUrl }),
178
+ targetBranch: observation.targetBranch,
133
179
  localCommit: request.expectedLocalCommit,
134
180
  headCommit: observation.headCommit,
135
181
  ...(observation.remoteCommit === undefined ? {} : { remoteCommit: observation.remoteCommit }),
@@ -145,7 +191,7 @@ function commitPublicationVerification(store, request, observation, now) {
145
191
  }
146
192
  function parsePublicationVerificationReference(args) {
147
193
  const usage = "Task publication verify usage: "
148
- + "yui task publication verify (<task>/<publication-id> | <task> <publication-id>).";
194
+ + "yui task publication verify (<task>/<publication-id> | <task> <publication-id>) [--observation <json>].";
149
195
  if (args.length !== 1 && args.length !== 2)
150
196
  throw usageError(usage);
151
197
  return resolveTaskRecordReference(args.length === 1 ? args[0] : `${args[0]}/${args[1]}`, { kind: "publicationReference", label: "Publication reference" });
@@ -27,15 +27,17 @@ export function renderTaskRemoteDelivery(delivery, indent = "") {
27
27
  const lines = [
28
28
  `${indent}Remote delivery: ${delivery.status}`,
29
29
  `${indent}Expected heads: ${source}`,
30
- `${indent}All merged: ${delivery.allMerged ? "yes" : "no"}`,
31
- `${indent}All verified: ${delivery.allVerified ? "yes" : "no"}`,
32
- `${indent}Code Projects: ${delivery.mergedProjectCount}/${delivery.codeProjectCount} merged; ${delivery.verifiedProjectCount}/${delivery.codeProjectCount} verified`,
30
+ `${indent}Required code Projects all merged: ${delivery.allMerged ? "yes" : "no"}`,
31
+ `${indent}Required code Projects all verified: ${delivery.allVerified ? "yes" : "no"}`,
32
+ `${indent}Required code Projects: ${delivery.mergedProjectCount}/${delivery.requiredCodeProjectCount} merged; ${delivery.verifiedProjectCount}/${delivery.requiredCodeProjectCount} verified; excluded=${delivery.excludedProjectCount}`,
33
+ ...(delivery.requiredCodeProjectCount === 0 ? [`${indent}No delivery obligation.`] : []),
33
34
  `${indent}Archive --integrated coverage: ${delivery.integratedCoverageSatisfied ? "satisfied" : "blocked"}`,
34
35
  ...(delivery.archiveDisposition === null
35
36
  ? []
36
37
  : [`${indent}Archive disposition: ${delivery.archiveDisposition}`]),
37
38
  `${indent}Projects:${delivery.projects.length === 0 ? " none" : ""}`,
38
39
  ...delivery.projects.map((project) => (`${indent}- ${project.directory} (${project.projectId}): `
40
+ + `requirement=${project.deliveryRequirement}; scopeDecision=${project.scopeDecision?.id ?? "none"}; `
39
41
  + `expected=${shortCommit(project.expectedLocalCommit)}; `
40
42
  + `candidate=${shortCommit(project.deliveryLocalCommit)}; `
41
43
  + `adoption=${project.adoption?.id ?? "none"}; `
@@ -300,9 +300,9 @@ export function contextSnapshotDeltaRefIds(store, snapshot) {
300
300
  throw new Error(`Context Snapshot parent is missing or drifted: ${snapshot.parentRef.id}.`);
301
301
  }
302
302
  validateContextSnapshot(parent);
303
- const previous = new Map(parent.refs.map((ref) => [ref.refId, ref]));
303
+ const previous = new Map(parent.refs.map((ref) => [`${ref.store}:${ref.refId}`, ref]));
304
304
  const changed = snapshot.refs.filter((ref) => {
305
- const before = previous.get(ref.refId);
305
+ const before = previous.get(`${ref.store}:${ref.refId}`);
306
306
  return before === undefined
307
307
  || before.digest !== ref.digest
308
308
  || before.revision !== ref.revision
@@ -385,6 +385,7 @@ export function runContextEntry(pack) {
385
385
  activeRuns: liveTaskState.activeRuns.length, activeTaskReviews: liveTaskState.activeTaskReviews.length }
386
386
  },
387
387
  reads: {
388
+ inbox: "yui inbox list --view current --json",
388
389
  refs: `yui task run context ${pack.identity.taskId}/${pack.identity.runId} --view refs`,
389
390
  full: `yui task run context ${pack.identity.taskId}/${pack.identity.runId} --view full`,
390
391
  input: `yui task run context ${pack.identity.taskId}/${pack.identity.runId} --view input`,
@@ -440,7 +441,7 @@ export function expandRunContextRef(store, taskId, runId, refId, refStore) {
440
441
  export function buildRunContextDelta(store, taskId, runId, after) {
441
442
  const pack = buildRunContextPack(store, taskId, runId);
442
443
  if (after === pack.digest || after === pack.snapshot.digest) {
443
- return Object.freeze({ schemaVersion: 1, after, cursor: pack.digest, refs: [] });
444
+ return Object.freeze({ schemaVersion: 1, after, cursor: pack.digest, refs: [], removedRefs: [], complete: true });
444
445
  }
445
446
  const run = requireExactRun(store, taskId, runId);
446
447
  const snapshot = readRunContextSnapshot(store, run);
@@ -449,11 +450,31 @@ export function buildRunContextDelta(store, taskId, runId, after) {
449
450
  || (parentDigest === undefined && after === "none"))) {
450
451
  throw new Error("AgentRun Context delta cursor is outside the frozen Snapshot lineage.");
451
452
  }
453
+ const parent = snapshot.parentRef === undefined ? null
454
+ : store.getContextSnapshot(taskId, snapshot.parentRef.id);
455
+ if (snapshot.parentRef !== undefined && (parent === null || parent.digest !== parentDigest
456
+ || parent.sequence !== snapshot.parentRef.sequence)) {
457
+ throw new Error("AgentRun Context parent changed or is unavailable; reload the exact current pack.");
458
+ }
459
+ if (parent !== null)
460
+ validateContextSnapshot(parent);
461
+ const identity = (ref) => `${ref.store}:${ref.refId}`;
462
+ const previous = new Map((parent?.refs ?? []).map(ref => [identity(ref), ref]));
463
+ const present = new Set(pack.pointers.map(identity));
452
464
  return Object.freeze({
453
465
  schemaVersion: 1,
454
466
  after,
455
467
  cursor: pack.digest,
456
- refs: pack.pointers.filter(ref => pack.deltaRefs.some(delta => delta.store === ref.store && delta.refId === ref.refId))
468
+ // deltaRefs is only a bounded launch hint. The immutable inventory (<=256)
469
+ // is the authority for a complete patch, including removals and changes
470
+ // beyond that hint's limit. Transport paginates large documents as usual.
471
+ refs: pack.pointers.filter(ref => {
472
+ const before = previous.get(identity(ref));
473
+ return before === undefined || contextContentDigest(before) !== contextContentDigest(ref);
474
+ }),
475
+ removedRefs: (parent?.refs ?? []).filter(ref => !present.has(identity(ref)))
476
+ .map(({ store, refId }) => ({ store, refId })),
477
+ complete: true
457
478
  });
458
479
  }
459
480
  function requireExactRun(store, taskId, runId) {
@@ -90,6 +90,7 @@ export function serializeRunInputEnvelope(value) {
90
90
  ]),
91
91
  "Use the Session Manifest's CLI entry. Fail closed if the exact Context is unavailable or mismatched.",
92
92
  "Then read the relevant referenced requirements and perform the assigned work. Context loading is not the deliverable.",
93
+ "Current continuation/steer inputs: yui inbox list --view current --json. Read and claim exact originals, then ack their disposition before the final report; this does not accept the assignment.",
93
94
  "During planning, fulfill the user's Draft discussion request without starting delivery before it is requested."
94
95
  ];
95
96
  const serialized = lines.join("\n");
@@ -2,7 +2,7 @@ import { runExecutionObservation } from "../agentRun/agentRun.js";
2
2
  import { isGitArtifactRefString, parseGitArtifactRef } from "../artifacts/gitArtifactRef.js";
3
3
  import { taskNotFound, usageError } from "../errors/cliError.js";
4
4
  import { expandTaskMessageResult } from "../message/message.js";
5
- import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
5
+ import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
6
6
  import { hasManagedIdentity } from "../runtime/managedIdentity.js";
7
7
  import { providerRetrySummary } from "../runtime/providerRetry.js";
8
8
  import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
@@ -12,6 +12,7 @@ import { contextContentDigest } from "./contextSnapshot.js";
12
12
  import { buildRunContextPack } from "./runContextPack.js";
13
13
  import { sourceRunContextValue } from "./sourceRunContext.js";
14
14
  import { boundedDocument, fitsReadBudget, summaryText, recordPage } from "../output/boundedRead.js";
15
+ import { taskBriefGuard, taskPhase } from "./taskPhase.js";
15
16
  const MAX_RECORDS = 64;
16
17
  const MAX_VALUE_BYTES = 2048;
17
18
  const MAX_PAGE_BYTES = 24 * 1024;
@@ -26,6 +27,9 @@ export function readTaskContext(store, taskId, environment = {}) {
26
27
  const sources = authorizedSources(reader, scope, true);
27
28
  const count = sources.reduce((total, source) => total + source.count, 0);
28
29
  const cursor = currentCursor(reader, taskId);
30
+ const phase = scope.allow === undefined ? taskPhase(reader, scope.task) : undefined;
31
+ const phaseSource = phase?.activation?.source;
32
+ const priorMessageId = phaseSource?.priorMessageId;
29
33
  // Reserve a small, classified view before ordinary history consumes the
30
34
  // page. Counts and samples come from the exact same authorized read.
31
35
  const attention = summarizeAttention(reader, taskId, scope.allow);
@@ -44,7 +48,20 @@ export function readTaskContext(store, taskId, environment = {}) {
44
48
  const content = valueBytes > MAX_VALUE_BYTES || ["role", "task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
45
49
  ? { ref: entry.ref, summary: summarize(entry.value), omitted: true }
46
50
  : { ...entry, omitted: false };
47
- const record = { ...content, ...(entry.ref.store === "run" ? {
51
+ const revisedAfterActivation = entry.ref.store === "task-message" && phaseSource?.ref !== undefined
52
+ && reader.queryContextRecords(taskId, {
53
+ family: "task-event", eventTypes: ["message.updated"], eventMessageId: entry.ref.refId,
54
+ afterSequence: sequence(phaseSource.ref.refId), limit: 0
55
+ }).count > 0;
56
+ const record = { ...content,
57
+ ...(entry.ref.store === "task-message" && phase?.activation?.disposition === "adopted"
58
+ && priorMessageId !== undefined ? {
59
+ phaseRelation: revisedAfterActivation ? "revised-after-activation-request"
60
+ : entry.ref.refId === phaseSource?.messageId ? "activation-source"
61
+ : Number(entry.ref.refId.slice("message-".length)) <= Number(priorMessageId.slice("message-".length))
62
+ ? "recorded-before-activation-request" : "recorded-after-activation-request"
63
+ } : {}),
64
+ ...(entry.ref.store === "run" ? {
48
65
  execution: runExecutionObservation(entry.value, reader.getTaskRoleSessionSet(taskId, entry.value.roleName)?.providerBinding, runtimeEvents)
49
66
  } : {}), ...(entry.ref.store === "role" ? {
50
67
  providerRetry: providerRetrySummary(reader.getTaskRoleSessionSet(taskId, entry.value.name)?.providerBinding)
@@ -58,6 +75,14 @@ export function readTaskContext(store, taskId, environment = {}) {
58
75
  const result = {
59
76
  taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
60
77
  view: "current",
78
+ ...(scope.allow === undefined ? {
79
+ phase,
80
+ briefWriteGuard: taskBriefGuard(reader, scope.task)
81
+ } : {}),
82
+ caller: scope.caller === undefined ? { kind: "unmanaged-or-global" } : {
83
+ role: scope.caller.roleName,
84
+ executionAuthority: currentManagedRuntime(reader, environment, taskId)?.executionAuthority ?? "historical-read-only"
85
+ },
61
86
  collections: Object.entries(sources.reduce((counts, source) => {
62
87
  counts[source.family] = (counts[source.family] ?? 0) + source.count;
63
88
  return counts;
@@ -68,7 +93,7 @@ export function readTaskContext(store, taskId, environment = {}) {
68
93
  commandDiscovery: {
69
94
  help: "yui help", describe: "yui --json help",
70
95
  skillCheck: "yui session skills --json",
71
- ownInbox: "yui inbox list --json",
96
+ ownInbox: "yui inbox list --view current --json",
72
97
  note: "Help and completion share the current caller's offline command view. Follow that view for operation-specific discovery; execution always revalidates authority."
73
98
  },
74
99
  detail: `task context inspect ${taskId} --store <store> --ref <refId> --digest <digest>`,
@@ -133,11 +158,16 @@ export function readTaskContextDelta(store, taskId, input, environment = {}) {
133
158
  const start = decode(input.after, taskId);
134
159
  const now = currentCursor(reader, taskId);
135
160
  const continuation = input.continuation === undefined ? undefined : decodePage(input.continuation, taskId);
161
+ if (continuation !== undefined && continuation.baseline !== encode(start)) {
162
+ throw usageError("Context continuation belongs to a different baseline; restart from the original starting cursor.");
163
+ }
136
164
  const bound = continuation ?? now;
137
165
  const after = continuation?.after ?? start.sequence;
138
- if (start.sequence > bound.sequence || after < start.sequence || after > bound.sequence
166
+ if (start.source !== now.source || bound.source !== now.source
167
+ || start.sequence > bound.sequence || after < start.sequence || after > bound.sequence
168
+ || start.revision > bound.revision
139
169
  || bound.sequence > now.sequence || start.revision > now.revision || bound.revision > now.revision) {
140
- throw usageError("Context cursor is outside the current Task history.");
170
+ throw usageError(`Context cursor is outside the current Task history; reread task context ${taskId} for a new bounded baseline.`);
141
171
  }
142
172
  const limit = input.limit ?? MAX_EVENTS;
143
173
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_EVENTS) {
@@ -163,11 +193,14 @@ export function readTaskContextDelta(store, taskId, input, environment = {}) {
163
193
  }
164
194
  const last = page.at(-1)?.ref.refId;
165
195
  const more = selected.count > page.length;
166
- const through = { taskId, sequence: bound.sequence, revision: bound.revision };
196
+ const through = { taskId, source: bound.source, sequence: bound.sequence, revision: bound.revision };
167
197
  return {
168
- taskId, events: page, count: selected.count, throughCursor: encode(through),
198
+ taskId, kind: "event-history", fromCursor: encode(start),
199
+ events: page, count: selected.count, complete: !more, throughCursor: encode(through),
200
+ stateChanged: start.revision !== bound.revision,
201
+ currentRead: `task context ${taskId}`,
169
202
  ...(more && last !== undefined
170
- ? { continuation: encode({ ...through, after: sequence(last) }) } : {}),
203
+ ? { continuation: encode({ ...through, after: sequence(last), baseline: encode(start) }) } : {}),
171
204
  observations: []
172
205
  };
173
206
  });
@@ -568,7 +601,8 @@ export function materialize(store, refId, value) {
568
601
  return { ref: { store, refId, revision: String(record.revision ?? record.updatedAt ?? record.createdAt ?? digest), digest }, value };
569
602
  }
570
603
  function currentCursor(store, taskId) {
571
- return { taskId, sequence: store.latestEventSequence(taskId), revision: store.getStateRevision() };
604
+ return { taskId, source: store.getHomeIdentity().homeId,
605
+ sequence: store.latestEventSequence(taskId), revision: store.getStateRevision() };
572
606
  }
573
607
  function sequence(id) {
574
608
  const result = /^event-(\d+)$/.exec(id);
@@ -584,20 +618,23 @@ function decode(value, taskId) {
584
618
  if (value.length > 1024)
585
619
  throw new Error("size");
586
620
  const cursor = JSON.parse(Buffer.from(value, "base64url").toString());
587
- if (cursor.taskId !== taskId || !Number.isSafeInteger(cursor.sequence) || cursor.sequence < 0
621
+ if (cursor.taskId !== taskId || typeof cursor.source !== "string"
622
+ || !Number.isSafeInteger(cursor.sequence) || cursor.sequence < 0
588
623
  || !Number.isSafeInteger(cursor.revision) || cursor.revision < 0)
589
624
  throw new Error("shape");
590
- return cursor;
625
+ return { taskId: cursor.taskId, source: cursor.source, sequence: cursor.sequence, revision: cursor.revision };
591
626
  }
592
627
  catch {
593
- throw usageError("Invalid Context cursor for this Task.");
628
+ throw usageError("Invalid or expired Context cursor for this Task; reread current Context for a new bounded baseline.");
594
629
  }
595
630
  }
596
631
  function decodePage(value, taskId) {
597
- const cursor = decode(value, taskId);
598
- if (!Number.isSafeInteger(cursor.after) || cursor.after < 0)
599
- throw usageError("Invalid Context continuation.");
600
- return cursor;
632
+ const base = decode(value, taskId);
633
+ const cursor = JSON.parse(Buffer.from(value, "base64url").toString());
634
+ if (!Number.isSafeInteger(cursor.after) || cursor.after < 0 || typeof cursor.baseline !== "string") {
635
+ throw usageError("Invalid or expired Context continuation; reread current Context and start a new bounded window.");
636
+ }
637
+ return { ...base, after: cursor.after, baseline: cursor.baseline };
601
638
  }
602
639
  function freeze(value) {
603
640
  if (value !== null && typeof value === "object") {
@@ -0,0 +1,99 @@
1
+ import { contextContentDigest } from "./contextSnapshot.js";
2
+ /** Existing immutable event payloads carry provenance, not a second authority
3
+ * record. No body or historical aggregate is copied into the current view. */
4
+ export function activationSourceEvent(store, task) {
5
+ const requestId = task.activationRequest?.operation.requestId;
6
+ if (requestId === undefined)
7
+ return undefined;
8
+ return store.queryContextRecords(task.id, {
9
+ family: "task-event", eventTypes: ["task.activation-requested"], eventRequestId: requestId,
10
+ descending: true, limit: 1
11
+ }).records[0]?.value;
12
+ }
13
+ export function taskPhase(store, task) {
14
+ const request = task.activationRequest;
15
+ const event = activationSourceEvent(store, task);
16
+ const messageId = event?.payload.sourceMessageId;
17
+ const message = messageId === undefined ? undefined : store.queryContextRecords(task.id, {
18
+ family: "task-message", ids: [messageId], limit: 1
19
+ }).records[0];
20
+ const body = message?.value?.body;
21
+ const status = event?.payload.sourceText !== undefined ? "available"
22
+ : messageId === undefined ? "missing"
23
+ : body === undefined ? "unavailable"
24
+ : contextContentDigest(body) === event?.payload.sourceBodyDigest ? "available" : "changed";
25
+ return {
26
+ status: task.status,
27
+ executionGate: task.executionGate.state,
28
+ brief: taskBriefFreshness(store, task),
29
+ activation: request === undefined ? null : {
30
+ requestId: request.operation.requestId, disposition: request.disposition,
31
+ actor: request.operation.actorId, effect: request.operation.effect,
32
+ ...(request.afterPlanningRun === undefined ? {} : { afterPlanningRun: request.afterPlanningRun }),
33
+ source: {
34
+ status,
35
+ ...(event === undefined ? {} : {
36
+ ref: { store: "task-event", refId: event.id, digest: contextContentDigest(event) },
37
+ read: `task event show ${task.id} ${event.id}`,
38
+ attribution: event.payload.sourceKind ?? "unspecified",
39
+ ...(event.payload.priorMessageId === undefined ? {} : { priorMessageId: event.payload.priorMessageId }),
40
+ ...(messageId === undefined ? {} : {
41
+ messageId, messageRead: `task message show ${task.id}/${messageId}`
42
+ })
43
+ }),
44
+ ...(status === "available" ? {} : {
45
+ recovery: "Read the exact activation event and original input; missing provenance is not a new prohibition or permission."
46
+ })
47
+ }
48
+ }
49
+ };
50
+ }
51
+ export function activationWakeRefs(store, task) {
52
+ const event = activationSourceEvent(store, task);
53
+ if (event === undefined)
54
+ return [];
55
+ return [
56
+ { type: "event", taskId: task.id, id: event.id },
57
+ ...(event.payload.sourceMessageId === undefined ? [] : [
58
+ { type: "message", taskId: task.id, id: event.payload.sourceMessageId }
59
+ ])
60
+ ];
61
+ }
62
+ const BRIEF_RELEVANT_EVENTS = [
63
+ "message.sent", "message.updated", "message.native-user-input", "brief.updated",
64
+ "decision.recorded", "decision.superseded", "task.project-delivery-decided", "input.requested", "input.answered", "input.cancelled", "input.auto-answered",
65
+ "run.completed", "run.failed", "run.cancelled", "job.state-changed", "work.accepted",
66
+ "runtime.role-delivery-failed", "run.retired",
67
+ "work.updated", "work.edited", "work.scope-updated", "work.rejected", "work.retired",
68
+ "review.completed", "review.failed", "integration.updated", "integration.completed", "integration.failed",
69
+ "milestone.added",
70
+ "task.activation-requested", "task.activation-adopted", "task.activated",
71
+ "task.execution-started", "task.execution-stopped", "task.reopened", "task.updated"
72
+ ];
73
+ function latestBriefChange(store, taskId) {
74
+ return store.queryContextRecords(taskId, {
75
+ family: "task-event", eventTypes: BRIEF_RELEVANT_EVENTS, descending: true, limit: 1
76
+ }).records[0]?.value;
77
+ }
78
+ function taskBriefFreshness(store, task) {
79
+ const brief = store.getTaskBrief(task.id);
80
+ const last = latestBriefChange(store, task.id);
81
+ return {
82
+ status: brief === null ? "missing" : last === undefined ? "unverified"
83
+ : last.type === "brief.updated" ? "current-at-last-semantic-write" : "refresh-needed",
84
+ ...(last === undefined ? {} : { changedBy: { store: "task-event", refId: last.id, type: last.type } }),
85
+ read: `task brief show ${task.id}`,
86
+ note: "Leader-maintained summary; lifecycle, gate and Session authority are separate live facts."
87
+ };
88
+ }
89
+ /** Optimistic guard for semantic edits, independent of wall clocks and noisy
90
+ * runtime heartbeats. New input/result/decision/progress invalidates an old
91
+ * writer even when its Brief text itself has not changed. */
92
+ export function taskBriefGuard(store, task) {
93
+ const latest = latestBriefChange(store, task.id)?.id;
94
+ return contextContentDigest({
95
+ taskId: task.id, status: task.status, gate: task.executionGate,
96
+ title: task.title, description: task.description, activation: task.activationRequest,
97
+ brief: store.getTaskBrief(task.id), latest
98
+ });
99
+ }
@@ -39,6 +39,7 @@ import { runConfigCommand } from "./commands/configCommands.js";
39
39
  import { runConfigOverview } from "./commands/configOverview.js";
40
40
  import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
41
41
  import { runDurableJobCommand } from "./commands/durableJobCommands.js";
42
+ import { runScheduleCommand } from "./commands/scheduleCommands.js";
42
43
  import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
43
44
  import { runGlobalRoleCommand, previewGlobalRoleAgentConfigurationMutation } from "./commands/globalRoleCommands.js";
44
45
  import { runJobCommand } from "./commands/jobCommands.js";
@@ -167,8 +168,6 @@ export async function main() {
167
168
  const discovery = discoveryCommandTree(process.env);
168
169
  invocationHome = home;
169
170
  const routedForFence = args.length === 0 ? undefined : routeInvocation(args);
170
- const managedInvocation = process.env.YUI_SESSION_SCOPE === "task"
171
- || process.env.YUI_SESSION_SCOPE === "global";
172
171
  const homeFreeInvocation = args.length === 0
173
172
  || (args[0] === "config" && (args[1] === "describe"
174
173
  || (args[1] === "completion" && args[2] === "candidates")))
@@ -678,29 +677,46 @@ export async function main() {
678
677
  });
679
678
  const workspaceCoordinator = new TaskWorkspaceCoordinator(store, workspacePreparer, runtime);
680
679
  if (resolved[0] === "web") {
681
- if (managedInvocation || process.env.YUI_ROLE || process.env.YUI_NATIVE_SESSION_ID) {
682
- throw usageError("The Web user ingress must be started from a local user terminal, not a managed Session.");
683
- }
680
+ if (hasManagedIdentity(process.env)) {
681
+ const current = requireManagedCaller(store, process.env);
682
+ if (current.scope !== "global" || current.roleName !== "operator") {
683
+ throw usageError("Only a local user or the current global Operator may manage Web listeners.");
684
+ }
685
+ }
686
+ const caller = resolveJobCaller(process.env, "");
687
+ const describe = (status) => status
688
+ ? `Web listener: ${status.state}\n`
689
+ + (status.alreadyRunning ? "Existing listener reused; credentials were not reloaded.\n" : "")
690
+ + (status.authentication === "basic" ? "Browser authentication: username yui; use the password from your private credential file.\n" : "")
691
+ + (status.urls ?? []).map(url => `Yui web control room: ${url}\n`).join("")
692
+ + (status.warnings ?? []).map(warning => `Warning: ${warning}\n`).join("")
693
+ : "Web is not running. Start it explicitly; Controller recovery does not restore listeners.\n";
684
694
  if (resolved.length === 2 && (resolved[1] === "--status" || resolved[1] === "--stop")) {
685
- const status = await callController(home, "web.status", {});
695
+ const status = await callController(home, "web.status", { caller });
686
696
  if (resolved[1] === "--status")
687
- emit(status ? `Yui web control room: ${status.url}\n` : "Web is not running.\n", false, status);
697
+ emit(describe(status), false, status);
688
698
  else {
689
- if (status)
690
- await callController(home, "web.stop", { id: status.id });
691
- emit("Web listener stopped; Controller and Agents are unchanged.\n");
699
+ if (status && !status.id)
700
+ throw usageError("Web listener is starting; query status before stopping.");
701
+ const result = status ? await callController(home, "web.stop", { id: status.id, caller }) : { stopped: false };
702
+ emit("Web listener stopped or already absent; Controller and Agents are unchanged.\n", false, result);
692
703
  }
693
704
  return;
694
705
  }
695
- if (jsonOutput)
696
- throw usageError("Web start does not support --json; use --status.");
697
- const options = parseWebCommandOptions(resolved.slice(1));
706
+ const detached = jsonOutput || caller.scope === "global" || resolved.includes("--detach");
707
+ const options = parseWebCommandOptions(resolved.slice(1).filter(arg => arg !== "--detach"));
698
708
  const id = randomUUID();
699
- await callController(home, "web.start", { ...options, id });
700
- const displayHost = options.host === "::1" ? "[::1]" : options.host;
701
- process.stdout.write(`Yui web control room: http://${displayHost}:${options.port}\n`);
709
+ const status = await callController(home, "web.start", { ...options, id, caller });
710
+ emit(describe(status), false, status);
711
+ if (detached || status.alreadyRunning)
712
+ return;
702
713
  await new Promise((resolve) => {
714
+ // The Controller owns the listening socket, so this CLI otherwise has
715
+ // no referenced handle: a Promise/signal handler alone does not keep
716
+ // Node alive for the foreground user's Ctrl-C.
717
+ const keepAlive = setInterval(() => { }, 60_000);
703
718
  const stop = () => {
719
+ clearInterval(keepAlive);
704
720
  process.off("SIGINT", stop);
705
721
  process.off("SIGTERM", stop);
706
722
  resolve();
@@ -708,7 +724,7 @@ export async function main() {
708
724
  process.once("SIGINT", stop);
709
725
  process.once("SIGTERM", stop);
710
726
  });
711
- await callController(home, "web.stop", { id });
727
+ await callController(home, "web.stop", { id, caller });
712
728
  return;
713
729
  }
714
730
  if (resolved[0] === "config") {
@@ -1963,6 +1979,11 @@ export async function main() {
1963
1979
  emit(runJobCommand(resolved.slice(1), store, { runtime }));
1964
1980
  return;
1965
1981
  }
1982
+ if (resolved[0] === "schedule") {
1983
+ const result = await runScheduleCommand(resolved.slice(1), { home, environment: process.env });
1984
+ emit(JSON.stringify(result, null, 2), false, result);
1985
+ return;
1986
+ }
1966
1987
  if (resolved[0] === "job") {
1967
1988
  emit(await runDurableJobCommand(resolved.slice(1), {
1968
1989
  home,