@zq-silk/yui 2.10.0 → 2.12.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 (90) hide show
  1. package/dist/artifacts/artifactCapability.js +14 -8
  2. package/dist/artifacts/artifactCommitLock.js +7 -5
  3. package/dist/artifacts/jobArtifact.js +93 -0
  4. package/dist/artifacts/jobArtifactTransport.js +13 -0
  5. package/dist/artifacts/taskArtifactRepository.js +58 -10
  6. package/dist/cli/callerDiscovery.js +18 -0
  7. package/dist/cli/commandCatalog.js +45 -14
  8. package/dist/cli/commandDiscovery.js +2 -3
  9. package/dist/commands/durableJobCommands.js +31 -6
  10. package/dist/commands/globalRoleCommands.js +5 -0
  11. package/dist/commands/scheduleCommands.js +14 -3
  12. package/dist/commands/taskCommands.js +5 -2
  13. package/dist/context/taskCatalog.js +6 -1
  14. package/dist/controlPlaneCli.js +12 -12
  15. package/dist/controller/agentJobControl.js +391 -0
  16. package/dist/controller/controller.js +24 -3
  17. package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
  18. package/dist/controller/globalInputDelivery.js +8 -1
  19. package/dist/controller/jobArtifactControl.js +111 -0
  20. package/dist/controller/jobControl.js +4 -0
  21. package/dist/controller/jobDefinitionControl.js +191 -19
  22. package/dist/controller/jobSupervisor.js +83 -3
  23. package/dist/controller/jobTaskDelegationControl.js +193 -0
  24. package/dist/controller/runtime.js +7 -1
  25. package/dist/controller/scheduleControl.js +83 -20
  26. package/dist/integration/gitIntegrationService.js +2 -0
  27. package/dist/job/agentJob.js +131 -0
  28. package/dist/job/agentJobObservation.js +61 -0
  29. package/dist/job/agentRoleScope.js +33 -0
  30. package/dist/job/durableJob.js +63 -28
  31. package/dist/job/jobAssignmentScope.js +1 -0
  32. package/dist/job/jobBusinessResult.js +72 -0
  33. package/dist/job/jobCleanupInspection.js +4 -0
  34. package/dist/job/jobDefinition.js +26 -2
  35. package/dist/job/jobRunner.js +4 -3
  36. package/dist/job/projectRefreshRunner.js +54 -18
  37. package/dist/job/projectSync.js +162 -0
  38. package/dist/job/scheduleService.js +55 -0
  39. package/dist/job/taskDelegation.js +48 -0
  40. package/dist/message/messageContinuation.js +1 -1
  41. package/dist/resources/projectResourceService.js +1 -1
  42. package/dist/resources/resourceGc.js +1 -1
  43. package/dist/role/role.js +22 -12
  44. package/dist/runtime/managedIdentity.js +5 -3
  45. package/dist/runtime/sessionCaller.js +9 -10
  46. package/dist/schedule/schedule.js +65 -15
  47. package/dist/scheduler/wakeReason.js +4 -0
  48. package/dist/storage/agentJobSchema.js +10 -0
  49. package/dist/storage/maintenanceCaller.js +17 -25
  50. package/dist/storage/nativeSessionLookup.js +23 -0
  51. package/dist/storage/recordValidation.js +2 -0
  52. package/dist/storage/sessionHistorySchema.js +17 -0
  53. package/dist/storage/sqliteSchema.js +96 -11
  54. package/dist/storage/sqliteStore.js +71 -4
  55. package/dist/storage/storageVersions.js +1 -1
  56. package/dist/storage/storeRpc.js +4 -0
  57. package/dist/storage/taskCatalog.js +10 -2
  58. package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
  59. package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
  60. package/dist/task/archivePreflight.js +15 -0
  61. package/dist/task/completionReadiness.js +7 -0
  62. package/dist/task/task.js +17 -1
  63. package/dist/verification/verificationGateService.js +4 -0
  64. package/dist/web/assets/client/app/catalog.js +16 -0
  65. package/dist/web/assets/client/i18n/en.js +29 -2
  66. package/dist/web/assets/client/i18n/zh-CN.js +29 -2
  67. package/dist/web/assets/client/layout/manageShell.js +9 -0
  68. package/dist/web/assets/client/layout/router.js +1 -0
  69. package/dist/web/assets/client/views/jobs.js +166 -37
  70. package/dist/web/assets/client/views/task/files.js +5 -4
  71. package/dist/web/assets/client/views/task/page.js +5 -1
  72. package/dist/web/taskPreviews.js +2 -2
  73. package/dist/web/webJobs.js +36 -11
  74. package/dist/web/webMaterials.js +21 -9
  75. package/dist/web/webServer.js +6 -5
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/authorization.md +130 -0
  78. package/docs/job-runs.md +392 -17
  79. package/docs/managed-turn-and-session-runtime.md +10 -5
  80. package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
  81. package/docs/release-workflow.md +79 -0
  82. package/docs/release-workflow.zh-CN.md +58 -0
  83. package/docs/schedules.md +69 -15
  84. package/docs/storage-baseline.md +45 -1
  85. package/docs/storage-baseline.zh-CN.md +33 -1
  86. package/docs/testing/verification-levels.md +2 -2
  87. package/docs/testing/verification-levels.zh-CN.md +2 -2
  88. package/package.json +1 -1
  89. package/skills/yui-runtime/SKILL.md +12 -5
  90. package/skills/yui-runtime/VERSION +1 -1
@@ -33,6 +33,15 @@ function requireTextContent(content) {
33
33
  * commit exactly that path, and return the resulting commit-pinned reference.
34
34
  */
35
35
  export async function saveArtifactCapability(home, taskId, input) {
36
+ const bytes = artifactInputBytes(input);
37
+ return saveArtifactFile(home, taskId, {
38
+ relativePath: input.relativePath, bytes,
39
+ ...(input.message === undefined ? {} : { message: input.message }),
40
+ ...(input.expectedHead === undefined ? {} : { expectedHead: input.expectedHead })
41
+ });
42
+ }
43
+ /** Shared bounded text/static-image input; never executes HTML or scripts. */
44
+ export function artifactInputBytes(input) {
36
45
  if (input.encoding !== undefined && input.encoding !== "utf8" && input.encoding !== "base64") {
37
46
  throw new Error("Artifact encoding must be utf8 or base64.");
38
47
  }
@@ -50,12 +59,7 @@ export async function saveArtifactCapability(home, taskId, input) {
50
59
  else {
51
60
  bytes = requireTextContent(input.content);
52
61
  }
53
- return saveArtifactFile(home, taskId, {
54
- relativePath: input.relativePath,
55
- bytes,
56
- ...(input.message === undefined ? {} : { message: input.message }),
57
- ...(input.expectedHead === undefined ? {} : { expectedHead: input.expectedHead })
58
- });
62
+ return bytes;
59
63
  }
60
64
  /**
61
65
  * `artifact.read`: read `relativePath` at HEAD, or at a pinned `commit` for
@@ -68,7 +72,10 @@ export async function readArtifactCapability(home, taskId, input) {
68
72
  throw new Error(`Artifact repository is unavailable for ${taskId}.`);
69
73
  const safeRelative = safeRelativeArtifactPath(input.relativePath);
70
74
  const content = await repo.read(safeRelative, input.commit === undefined ? undefined : requireCommitId(input.commit));
71
- const image = isRasterArtifact(safeRelative) ? rasterMetadata(content.bytes) : null;
75
+ return { taskId, ...presentArtifactContent(content) };
76
+ }
77
+ export function presentArtifactContent(content) {
78
+ const image = isRasterArtifact(content.relativePath) ? rasterMetadata(content.bytes) : null;
72
79
  let text;
73
80
  if (image)
74
81
  text = content.bytes.toString("base64");
@@ -83,7 +90,6 @@ export async function readArtifactCapability(home, taskId, input) {
83
90
  throw new Error("Binary artifact is not supported as text.");
84
91
  }
85
92
  return {
86
- taskId,
87
93
  relativePath: content.relativePath,
88
94
  commit: content.commit,
89
95
  content: text,
@@ -1,5 +1,7 @@
1
1
  import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { setTimeout } from "node:timers/promises";
4
+ import { safePathSegment } from "./artifactPaths.js";
3
5
  import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner.js";
4
6
  /**
5
7
  * Short per-Task commit fence for artifact repositories.
@@ -11,8 +13,8 @@ import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner
11
13
  * the commit critical section for one Task.
12
14
  *
13
15
  * The Project maintenance fence waits asynchronously for longer operations.
14
- * An artifact save is short, so this synchronous critical section briefly
15
- * retries a contended fence before giving up. It otherwise shares the same
16
+ * An artifact save is short, so acquisition asynchronously waits for a contended
17
+ * fence, allowing the live holder's Git subprocess to finish. It shares the same
16
18
  * crash-safe design: an exclusive directory under the Home's `locks/` area
17
19
  * (O_EXCL via mkdir), an owner file recording PID + process start time so a
18
20
  * recycled PID can never pass for the original holder, and reclaim of a stale
@@ -37,7 +39,7 @@ export class ArtifactCommitLockedError extends Error {
37
39
  }
38
40
  /** Directory of one Task's artifact commit fence, below the Home's locks area. */
39
41
  export function artifactCommitLockPath(home, taskId) {
40
- return join(home, "locks", "task-artifacts", `${taskId}.lock`);
42
+ return join(home, "locks", "task-artifacts", `${safePathSegment(taskId)}.lock`);
41
43
  }
42
44
  /**
43
45
  * Acquire one Task's artifact commit fence. Returns the release function;
@@ -46,7 +48,7 @@ export function artifactCommitLockPath(home, taskId) {
46
48
  * a stale (dead) holder is reclaimed. There is no in-process reentrancy: every
47
49
  * acquisition contends.
48
50
  */
49
- export function acquireArtifactCommitLock(home, taskId) {
51
+ export async function acquireArtifactCommitLock(home, taskId) {
50
52
  const lock = artifactCommitLockPath(home, taskId);
51
53
  const ownerIdentity = currentFileLockOwner();
52
54
  mkdirSync(join(home, "locks", "task-artifacts"), { recursive: true, mode: 0o700 });
@@ -69,7 +71,7 @@ export function acquireArtifactCommitLock(home, taskId) {
69
71
  reclaimStaleArtifactCommitLock(lock);
70
72
  if (Date.now() >= deadline)
71
73
  throw new ArtifactCommitLockedError(taskId);
72
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ARTIFACT_COMMIT_LOCK_RETRY_MS);
74
+ await setTimeout(ARTIFACT_COMMIT_LOCK_RETRY_MS);
73
75
  }
74
76
  }
75
77
  }
@@ -0,0 +1,93 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { join } from "node:path";
4
+ import { requireKnownFields } from "../domain/validation.js";
5
+ import { jobScopeKey } from "../job/jobDefinition.js";
6
+ import { runReference } from "../job/jobBusinessResult.js";
7
+ import { requireCommitId } from "./managedGit.js";
8
+ import { safeRelativeArtifactPath } from "./artifactPaths.js";
9
+ import { openTaskArtifactRepository, openVersionedArtifactRepository } from "./taskArtifactRepository.js";
10
+ function identity(value) {
11
+ if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(value)) {
12
+ throw new Error("Invalid artifact owner/Run identity.");
13
+ }
14
+ return value;
15
+ }
16
+ function validateScope(scope) {
17
+ requireKnownFields(scope, scope?.kind === "task" ? ["kind", "taskId"] : ["kind"], "Artifact scope");
18
+ if (scope.kind !== "global" && scope.kind !== "task")
19
+ throw new Error("Invalid artifact scope.");
20
+ jobScopeKey(scope);
21
+ }
22
+ export function validateJobArtifactReference(ref, run) {
23
+ requireKnownFields(ref, ["kind", "owner", "sourceRun", "version", "relativePath", "digest"], "Artifact reference");
24
+ if (ref.kind !== "artifact")
25
+ throw new Error("Expected an explicit artifact reference.");
26
+ requireKnownFields(ref.sourceRun, ["scope", "runId"], "Artifact source Run");
27
+ validateScope(ref.sourceRun.scope);
28
+ identity(ref.sourceRun.runId);
29
+ if (ref.owner.kind === "job") {
30
+ requireKnownFields(ref.owner, ["kind", "scope", "jobId"], "Artifact Job owner");
31
+ validateScope(ref.owner.scope);
32
+ identity(ref.owner.jobId);
33
+ if (!isDeepStrictEqual(ref.owner.scope, ref.sourceRun.scope))
34
+ throw new Error("Artifact owner/source scope mismatch.");
35
+ if (run && ref.owner.jobId !== run.definitionId)
36
+ throw new Error("Artifact Job owner mismatch.");
37
+ }
38
+ else if (ref.owner.kind === "task") {
39
+ requireKnownFields(ref.owner, ["kind", "taskId"], "Artifact Task owner");
40
+ jobScopeKey({ kind: "task", taskId: ref.owner.taskId });
41
+ }
42
+ else
43
+ throw new Error("Invalid artifact owner.");
44
+ if (run && !isDeepStrictEqual(ref.sourceRun, runReference(run)))
45
+ throw new Error("Artifact source Run mismatch.");
46
+ if (requireCommitId(ref.version) !== ref.version)
47
+ throw new Error("Artifact version must be an exact commit.");
48
+ safeRelativeArtifactPath(ref.relativePath);
49
+ if (ref.relativePath.length > 512)
50
+ throw new Error("Artifact reference path exceeds 512 characters.");
51
+ if (typeof ref.digest !== "string" || !/^[a-f0-9]{64}$/u.test(ref.digest))
52
+ throw new Error("Invalid artifact digest.");
53
+ return ref;
54
+ }
55
+ export function jobArtifactOwner(run) {
56
+ return { kind: "job", scope: runReference(run).scope, jobId: run.definitionId };
57
+ }
58
+ /** Each Run has a distinct working tree and commit fence even for identical paths.
59
+ * The retained storage is unrelated to execution workspaces/logs/checkpoints. */
60
+ export function openJobArtifactRepository(home, owner, sourceRun) {
61
+ validateScope(owner.scope);
62
+ validateScope(sourceRun.scope);
63
+ if (!isDeepStrictEqual(owner.scope, sourceRun.scope))
64
+ throw new Error("Artifact scope mismatch.");
65
+ const scopePath = owner.scope.kind === "global" ? ["global"] : ["task", owner.scope.taskId];
66
+ const path = join(home, "job-artifacts", ...scopePath, identity(owner.jobId), identity(sourceRun.runId));
67
+ const lockKey = `job-${createHash("sha256").update(JSON.stringify([jobScopeKey(owner.scope), owner.jobId, sourceRun.runId])).digest("hex")}`;
68
+ return openVersionedArtifactRepository(home, lockKey, path);
69
+ }
70
+ export async function readJobArtifactReference(home, ref) {
71
+ validateJobArtifactReference(ref);
72
+ const repo = ref.owner.kind === "task"
73
+ ? openTaskArtifactRepository(home, ref.owner.taskId)
74
+ : openJobArtifactRepository(home, ref.owner, ref.sourceRun);
75
+ const content = await repo.read(ref.relativePath, ref.version);
76
+ if (content.digest !== ref.digest)
77
+ throw new Error("Artifact digest mismatch; preserved evidence was not replaced.");
78
+ return content;
79
+ }
80
+ export function validateRunArtifacts(run) {
81
+ if (run.artifacts === undefined)
82
+ return;
83
+ if (!Array.isArray(run.artifacts) || run.artifacts.length > 256
84
+ || Buffer.byteLength(JSON.stringify(run.artifacts)) > 128 * 1024)
85
+ throw new Error("Run artifacts exceed the bounded reference limit.");
86
+ for (const ref of run.artifacts)
87
+ validateJobArtifactReference(ref, run);
88
+ }
89
+ export function artifactsAppendOnly(before, after) {
90
+ const old = before.artifacts ?? [];
91
+ const next = after.artifacts ?? [];
92
+ return next.length >= old.length && old.every((ref, i) => isDeepStrictEqual(ref, next[i]));
93
+ }
@@ -0,0 +1,13 @@
1
+ /** Conservative encoded payload bound inside the 1 MiB Controller envelope.
2
+ * JSON escaping/base64 and reference metadata count, not just raw file bytes.
3
+ * Repository limits stay independent; large retained text uses paged preview. */
4
+ export const MAX_JOB_ARTIFACT_JSON_BYTES = 256 * 1024;
5
+ // Leave room for the fixed reference and presentation metadata added on reads.
6
+ export const MAX_JOB_ARTIFACT_RESPONSE_BYTES = 512 * 1024;
7
+ export function boundedJobArtifactPayload(value, label, limit = MAX_JOB_ARTIFACT_JSON_BYTES) {
8
+ if (Buffer.byteLength(JSON.stringify(value), "utf8") > limit) {
9
+ throw new Error(`${label} exceeds the Job artifact ${limit / 1024} KiB encoded JSON transport limit. `
10
+ + "Use smaller material or paged artifact-preview for retained text; source Task material remains available through its owning surface.");
11
+ }
12
+ return value;
13
+ }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
- import { mkdir, writeFile } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
3
+ import { lstat, mkdir, writeFile } from "node:fs/promises";
4
+ import { dirname, join, relative } from "node:path";
5
5
  import { supportedGit } from "../repository/gitCompatibility.js";
6
6
  import { acquireArtifactCommitLock } from "./artifactCommitLock.js";
7
7
  import { resolveContainedArtifactPath, safeRelativeArtifactPath, taskArtifactRepoPath } from "./artifactPaths.js";
@@ -90,13 +90,35 @@ export class ArtifactHeadConflictError extends Error {
90
90
  */
91
91
  export function openTaskArtifactRepository(home, taskId) {
92
92
  const repoPath = taskArtifactRepoPath(home, taskId);
93
+ const repo = openVersionedArtifactRepository(home, taskId, repoPath);
94
+ return { ...repo, taskId, save: async (input) => ({ ...await repo.save(input), taskId }) };
95
+ }
96
+ /** Shared local-only storage. Callers derive paths from validated owning identities,
97
+ * never from a user-supplied filesystem location. Task layout/commits stay intact. */
98
+ export function openVersionedArtifactRepository(home, key, repoPath) {
99
+ const taskId = key; // Existing error fields retain their public spelling.
100
+ const checkBoundary = async () => {
101
+ await resolveContainedArtifactPath(home, `${relative(home, repoPath).split("\\").join("/")}/boundary-check`);
102
+ try {
103
+ const metadata = await lstat(join(repoPath, ".git"));
104
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
105
+ throw new Error("Artifact Git metadata must be its owned local directory, not a link/worktree.");
106
+ }
107
+ }
108
+ catch (error) {
109
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
110
+ throw error;
111
+ }
112
+ };
93
113
  const exists = () => existsSync(join(repoPath, ".git"));
94
114
  const head = async () => {
115
+ await checkBoundary();
95
116
  if (!exists())
96
117
  return null;
97
118
  return currentHead(repoPath);
98
119
  };
99
- const ensure = async () => {
120
+ const ensureUnlocked = async () => {
121
+ await checkBoundary();
100
122
  supportedGit();
101
123
  if (exists()) {
102
124
  await assertNoRemote(taskId, repoPath);
@@ -109,6 +131,15 @@ export function openTaskArtifactRepository(home, taskId) {
109
131
  await managedGit(repoPath, ["commit", "--allow-empty", "-m", "init artifact repo"]);
110
132
  await assertNoRemote(taskId, repoPath);
111
133
  };
134
+ const ensure = async () => {
135
+ const release = await acquireArtifactCommitLock(home, key);
136
+ try {
137
+ await ensureUnlocked();
138
+ }
139
+ finally {
140
+ release();
141
+ }
142
+ };
112
143
  const save = async (input) => {
113
144
  if (input.files.length === 0) {
114
145
  throw new Error("An artifact save must include at least one file.");
@@ -135,9 +166,9 @@ export function openTaskArtifactRepository(home, taskId) {
135
166
  }
136
167
  seen.add(file.relativePath);
137
168
  }
138
- await ensure();
139
- const release = acquireArtifactCommitLock(home, taskId);
169
+ const release = await acquireArtifactCommitLock(home, key);
140
170
  try {
171
+ await ensureUnlocked();
141
172
  await assertNoRemote(taskId, repoPath);
142
173
  // Expected-HEAD check under the lock: a conflict stops without writing.
143
174
  if (input.expectedHead !== undefined) {
@@ -161,22 +192,23 @@ export function openTaskArtifactRepository(home, taskId) {
161
192
  "diff", "--cached", "--quiet", "--", ...pathspecs
162
193
  ]);
163
194
  if (noChanges) {
164
- return { taskId, commit: await currentHead(repoPath), savedPaths: pathspecs };
195
+ return { commit: await currentHead(repoPath), savedPaths: pathspecs };
165
196
  }
166
197
  // `--only <paths>` records exactly these paths; any other WIP stays WIP.
167
198
  await managedGit(repoPath, ["commit", "--only", "-m", message, "--", ...pathspecs]);
168
- return { taskId, commit: await currentHead(repoPath), savedPaths: pathspecs };
199
+ return { commit: await currentHead(repoPath), savedPaths: pathspecs };
169
200
  }
170
201
  finally {
171
202
  release();
172
203
  }
173
204
  };
174
205
  const read = async (relativePath, commit) => {
206
+ await checkBoundary();
175
207
  const safeRelative = safeRelativeArtifactPath(relativePath);
176
208
  // A pinned commit reads frozen evidence; otherwise read the current HEAD.
177
209
  // Both resolve `<commit>:<path>`, an OBJECT spec (never the working tree, so
178
210
  // a read never follows a symlink or sees unrelated WIP).
179
- const pinned = commit === undefined ? await currentHead(repoPath) : requireCommitId(commit);
211
+ const pinned = await artifactCommit(repoPath, commit);
180
212
  const objectSpec = `${pinned}:${safeRelative}`;
181
213
  // `<commit>:<path>` for a DIRECTORY resolves to a tree, and `git show` would
182
214
  // print a tree listing rather than fail — silently returning directory
@@ -211,9 +243,10 @@ export function openTaskArtifactRepository(home, taskId) {
211
243
  };
212
244
  };
213
245
  const list = async (commit) => {
246
+ await checkBoundary();
214
247
  if (!exists())
215
248
  return [];
216
- const pinned = commit === undefined ? await currentHead(repoPath) : requireCommitId(commit);
249
+ const pinned = await artifactCommit(repoPath, commit);
217
250
  const output = await managedGit(repoPath, [
218
251
  "ls-tree", "-r", "-z", "--long", "--full-tree", pinned
219
252
  ]);
@@ -233,6 +266,7 @@ export function openTaskArtifactRepository(home, taskId) {
233
266
  return entries;
234
267
  };
235
268
  const workingChanges = async () => {
269
+ await checkBoundary();
236
270
  if (!exists())
237
271
  return [];
238
272
  const output = await managedGit(repoPath, [
@@ -247,11 +281,25 @@ export function openTaskArtifactRepository(home, taskId) {
247
281
  }
248
282
  return changes;
249
283
  };
250
- return { taskId, repoPath, ensure, exists, head, save, read, list, workingChanges };
284
+ return { repoPath, ensure, exists, head, save, read, list, workingChanges };
251
285
  }
252
286
  async function currentHead(repoPath) {
253
287
  return requireCommitId(await managedGit(repoPath, ["rev-parse", "HEAD^{commit}"]));
254
288
  }
289
+ async function artifactCommit(repoPath, value) {
290
+ if (value === undefined)
291
+ return currentHead(repoPath);
292
+ const commit = requireCommitId(value);
293
+ const type = await managedGit(repoPath, ["cat-file", "-t", commit]).catch(error => {
294
+ if (error instanceof ManagedGitError)
295
+ throw new Error(`Artifact commit ${commit} is unavailable.`, { cause: error });
296
+ throw error;
297
+ });
298
+ if (type.trim() !== "commit") {
299
+ throw new Error("Artifact version must name a commit, not another Git object.");
300
+ }
301
+ return commit;
302
+ }
255
303
  /**
256
304
  * Verify the repository is within the managed boundary before a write: NO remote
257
305
  * (network or file sync) AND no repo-local config that can execute an external
@@ -0,0 +1,18 @@
1
+ import { maintenanceCallerEnvironment } from "../storage/maintenanceCaller.js";
2
+ import { discoveryAudience } from "./commandDiscovery.js";
3
+ /** Complete role hints retain their offline template, not authentication.
4
+ * Native-ID-only callers need local lookup to select a template. Read only the
5
+ * selected Home's registry; failure yields unbound, never a public fallback.
6
+ * This observation is deliberately not written back to the process environment. */
7
+ export function callerDiscoveryAudience(home, env) {
8
+ if ((env.YUI_SESSION_SCOPE !== undefined && env.YUI_ROLE !== undefined)
9
+ || (env.CODEX_THREAD_ID === undefined && env.YUI_NATIVE_SESSION_ID === undefined)) {
10
+ return discoveryAudience(env);
11
+ }
12
+ try {
13
+ return discoveryAudience(maintenanceCallerEnvironment(home, env, false));
14
+ }
15
+ catch {
16
+ return "unbound";
17
+ }
18
+ }
@@ -397,8 +397,8 @@ const taskChildren = [
397
397
  {
398
398
  name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
399
399
  summary: "Discover Tasks through a bounded, filterable catalog.",
400
- usage: "yui task list [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
401
- options: ["--all", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
400
+ usage: "yui task list [--all] [--source <ordinary|job>] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
401
+ options: ["--all", "--source", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
402
402
  },
403
403
  {
404
404
  name: "search", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
@@ -1653,18 +1653,47 @@ export const ROOT_COMMAND = buildNode({
1653
1653
  ]
1654
1654
  },
1655
1655
  {
1656
- name: "job", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1656
+ name: "job", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
1657
1657
  summary: "Manage stable Job definitions and exact execution Runs (not scheduler wakes).",
1658
- sections: [{ id: "manage", title: "Commands", entries: ["define", "update", "list", "show", "run", "runs", "run-get", "logs", "run-cancel", "run-acknowledge", "start", "get", "cancel", "acknowledge"] }],
1658
+ sections: [{ id: "manage", title: "Commands", entries: ["define", "update", "list", "show", "run", "runs", "run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "run-claim", "run-context", "run-report", "run-continue", "session-inspect", "session-create", "session-stop", "session-new", "logs", "run-cancel", "run-acknowledge", "artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register", "start", "get", "cancel", "acknowledge"] }],
1659
1659
  children: [
1660
- ...["define", "update", "list", "show", "run", "runs", "run-get", "logs", "run-cancel", "run-acknowledge"].map(name => ({
1661
- name, discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1662
- summary: `${name} a stable Job or exact Run.`,
1660
+ ...["run-claim", "run-context", "run-report", "run-continue", "session-inspect", "session-create", "session-stop", "session-new"].map(name => ({
1661
+ name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
1662
+ summary: name === "run-report" ? "Record the exact native attempt's explicit Job business result."
1663
+ : name === "run-continue" ? "Continue an unfinished Run only after its exact prior native terminal."
1664
+ : name === "session-new" ? "Replace a proven-ended Job-owned Session, retaining Job input and results."
1665
+ : `${name} for a bounded Agent Job execution.`,
1666
+ usage: `yui job ${name} (--task <id>|--global) --job <id>`
1667
+ + (name.startsWith("run-") ? " --run <run-id>" : " [--run <isolated-run-id>]")
1668
+ + (name === "run-report" ? " --result <business-result-json>" : "")
1669
+ + (name === "run-continue" ? " --expected-attempt <prior-native-attempt-id>" : "")
1670
+ + (name === "session-new" || name === "session-stop" ? " --expected-session <native-id>" : ""),
1671
+ options: ["--task", "--global", "--job", "--run", "--result", "--expected-session", "--expected-attempt"]
1672
+ })),
1673
+ ...["artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register"].map(name => ({
1674
+ name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
1675
+ summary: name === "artifact-save" ? "Save bounded formal material and publish an exact owned Run reference."
1676
+ : name === "artifact-register" ? "Register already-saved evidence without replaying execution."
1677
+ : name === "artifact-read" || name === "artifact-preview" ? "Read/preview one registered immutable artifact (never HEAD)."
1678
+ : "List published Run history and retained repository evidence.",
1679
+ usage: `yui job ${name} (--task <id>|--global) --job <id> --run <run-id>`
1680
+ + (name === "artifact-save" ? " --artifact <save-json>" : "")
1681
+ + (["artifact-read", "artifact-preview", "artifact-register"].includes(name) ? " --reference <reference-json>" : "")
1682
+ + (name === "artifact-preview" ? " [--offset <text-offset>]" : ""),
1683
+ options: ["--task", "--global", "--job", "--run", "--artifact", "--reference", "--offset"]
1684
+ })),
1685
+ ...["define", "update", "list", "show", "run", "runs", "run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "logs", "run-cancel", "run-acknowledge"].map(name => ({
1686
+ name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
1687
+ summary: name === "run-result" ? "Read an exact Run's business outcome and admitted snapshot."
1688
+ : name === "run-evidence" ? "Read original process evidence separately from the business outcome."
1689
+ : name === "run-confirm" ? "Collect retained results for an exact Run without starting or replaying work."
1690
+ : name === "run-delegation" ? "Read the exact execution Task/WorkItem and attention projection; never a grant."
1691
+ : `${name} a stable Job or exact Run.`,
1663
1692
  usage: `yui job ${name} (--task <id>|--global)${name === "list" ? "" : " --job <id>"}`
1664
1693
  + (name === "define" || name === "update" ? " --title <text> --input <configuration-json>" : "")
1665
1694
  + (name === "update" ? " --revision <current>" : "")
1666
1695
  + (name === "run" ? " --request-id <key> [--retry-of <run-id>]" : "")
1667
- + (["run-get", "logs", "run-cancel", "run-acknowledge"].includes(name) ? " --run <run-id>" : "")
1696
+ + (["run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "logs", "run-cancel", "run-acknowledge"].includes(name) ? " --run <run-id>" : "")
1668
1697
  })),
1669
1698
  { name: "start", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Start a DurableJob under an explicit request identity.", usage: "yui job start --request-id <id> --task <id> --project <project> --head <sha> --workspace <dir> --step <name>=<command> [--step ...] [--owner task|work-item:<id>|integration-attempt:<id>] [--env <k=v>...]" },
1670
1699
  { name: "get", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Show a DurableJob record and its terminal result.", usage: "yui job get --task <id> --job <job-id>" },
@@ -1673,16 +1702,18 @@ export const ROOT_COMMAND = buildNode({
1673
1702
  ]
1674
1703
  },
1675
1704
  {
1676
- name: "schedule", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1705
+ name: "schedule", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] },
1677
1706
  summary: "Manage persistent time plans referencing scoped Jobs.",
1678
- sections: [{ id: "manage", title: "Commands", entries: ["create", "list", "show", "update", "enable", "disable"] }],
1679
- children: ["create", "list", "show", "update", "enable", "disable"].map(name => ({
1680
- name, discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] },
1681
- summary: `${name} a Schedule; disabling never cancels an admitted Run.`,
1707
+ sections: [{ id: "manage", title: "Commands", entries: ["create", "list", "show", "update", "enable", "disable", "revoke"] }],
1708
+ children: ["create", "list", "show", "update", "enable", "disable", "revoke"].map(name => ({
1709
+ name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] },
1710
+ summary: `${name} a Schedule; disable stops future dispatch, revoke also denies queued service starts.`,
1682
1711
  usage: `yui schedule ${name} (--global|--task <id>)${name === "list" ? "" : " --schedule <id>"}`
1683
1712
  + (name === "create" ? " --job <id> --rule <json>" : "")
1684
1713
  + (name === "update" ? " --rule <json> [--job <id>]" : "")
1685
- + (["update", "enable", "disable"].includes(name) ? " --revision <observed>" : "")
1714
+ + (["create", "update"].includes(name) ? " [--target <json>]" : "")
1715
+ + (["create", "update", "enable"].includes(name) ? " [--authorization session|service]" : "")
1716
+ + (["update", "enable", "disable", "revoke"].includes(name) ? " --revision <observed>" : "")
1686
1717
  }))
1687
1718
  },
1688
1719
  {
@@ -16,8 +16,7 @@ export function discoveryAudience(env) {
16
16
  return env.YUI_ROLE === "leader" ? "leader" : "assignment";
17
17
  return "unbound";
18
18
  }
19
- export function discoveryCommandTree(env) {
20
- const audience = discoveryAudience(env);
19
+ export function discoveryCommandTree(env, audience = discoveryAudience(env)) {
21
20
  const project = (node) => {
22
21
  const children = node.children.map(project).filter((child) => child !== undefined);
23
22
  const eligible = node.discovery?.surface !== "runtime" && node.discovery?.audiences.includes(audience);
@@ -48,7 +47,7 @@ export function discoveryCommandTree(env) {
48
47
  const sections = [
49
48
  { id: "start", title: "Start work", entries: ["operator", "project"] },
50
49
  { id: "progress", title: "Inspect progress", entries: ["task", "jobs", "execution", "telemetry"] },
51
- { id: "participate", title: "Participate and control", entries: ["role", "session"] },
50
+ { id: "participate", title: "Participate and control", entries: ["role", "session", "job"] },
52
51
  { id: "maintain", title: "Configure and maintain", entries: ["help", "version", "setup", "start", "doctor", "config", "web", "controller", "resources", "update", "upgrade", "release"] }
53
52
  ];
54
53
  return { ...tree, sections: sections.map(section => ({ ...section,
@@ -3,6 +3,7 @@ import { ensureFileTaskController } from "../controller/clientRuntime.js";
3
3
  import { callFileTaskController } from "../controller/clientRuntime.js";
4
4
  import { acknowledgeDurableJob, cancelDurableJob, getDurableJob, startDurableJob } from "../controller/jobClient.js";
5
5
  import { resolveJobCaller, taskLocalActor } from "../task/taskAuthority.js";
6
+ import { boundedJobArtifactPayload } from "../artifacts/jobArtifactTransport.js";
6
7
  /**
7
8
  * The textual `--owner` forms accepted by `job start`. The public help text in
8
9
  * the command catalog must document exactly these forms.
@@ -15,7 +16,7 @@ export const JOB_OWNER_USAGE = "task|work-item:<id>|integration-attempt:<id>";
15
16
  */
16
17
  export async function runDurableJobCommand(args, options) {
17
18
  const [command, ...rest] = args;
18
- if (command !== undefined && ["define", "update", "list", "show", "run", "runs", "run-get", "run-cancel", "run-acknowledge", "logs"].includes(command)) {
19
+ if (command !== undefined && ["define", "update", "list", "show", "run", "runs", "run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "run-cancel", "run-acknowledge", "logs", "artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register", "run-claim", "run-context", "run-report", "run-continue", "session-inspect", "session-create", "session-stop", "session-new"].includes(command)) {
19
20
  return definitionCommand(command, rest, options);
20
21
  }
21
22
  if (command === "start")
@@ -39,7 +40,7 @@ async function definitionCommand(command, args, options) {
39
40
  const flags = new Map();
40
41
  for (let i = 0; i < args.length; i++) {
41
42
  const flag = args[i];
42
- if (!["--global", "--task", "--job", "--title", "--input", "--revision", "--request-id", "--retry-of", "--run"].includes(flag)
43
+ if (!["--global", "--task", "--job", "--title", "--input", "--revision", "--request-id", "--retry-of", "--run", "--artifact", "--reference", "--offset", "--result", "--expected-session", "--expected-attempt"].includes(flag)
43
44
  || flags.has(flag))
44
45
  throw usageError(`Unknown or repeated Job flag: ${flag}.`);
45
46
  if (flag === "--global") {
@@ -54,7 +55,10 @@ async function definitionCommand(command, args, options) {
54
55
  if (flags.has("--global") === flags.has("--task"))
55
56
  throw usageError("Choose exactly one of --task <id> or --global.");
56
57
  const taskId = flags.get("--task");
57
- const caller = resolveJobCaller(options.environment, taskId ?? "");
58
+ const executionRead = ["show", "runs", "run-get", "run-result", "run-context", "run-claim", "run-report",
59
+ "run-evidence", "run-confirm", "artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register"].includes(command);
60
+ const caller = resolveJobCaller(options.environment, taskId
61
+ ?? (executionRead ? options.environment?.YUI_TASK_ID : undefined) ?? "");
58
62
  const scope = taskId === undefined ? { kind: "global" } : { kind: "task", taskId };
59
63
  let configuration;
60
64
  if (flags.has("--input")) {
@@ -66,10 +70,31 @@ async function definitionCommand(command, args, options) {
66
70
  }
67
71
  }
68
72
  const requestId = flags.get("--request-id");
73
+ const parseObject = (flag) => {
74
+ if (!flags.has(flag))
75
+ return undefined;
76
+ try {
77
+ return JSON.parse(flags.get(flag));
78
+ }
79
+ catch {
80
+ throw usageError(`${flag} must be JSON.`);
81
+ }
82
+ };
83
+ const artifact = parseObject("--artifact");
84
+ if (artifact !== undefined)
85
+ boundedJobArtifactPayload(artifact, "Artifact save");
86
+ const reference = parseObject("--reference");
87
+ const resultInput = parseObject("--result");
69
88
  if (command === "run" && !requestId)
70
89
  throw usageError("job run requires --request-id.");
71
90
  const params = {
72
91
  op: command, scope, caller,
92
+ ...(artifact === undefined ? {} : { artifact }),
93
+ ...(reference === undefined ? {} : { reference }),
94
+ ...(resultInput === undefined ? {} : { result: resultInput }),
95
+ ...(flags.has("--expected-session") ? { expectedSessionId: flags.get("--expected-session") } : {}),
96
+ ...(flags.has("--expected-attempt") ? { expectedAttemptId: flags.get("--expected-attempt") } : {}),
97
+ ...(flags.has("--offset") ? { offset: Number(flags.get("--offset")) } : {}),
73
98
  ...(flags.has("--job") ? { id: flags.get("--job") } : {}),
74
99
  ...(flags.has("--title") ? { title: flags.get("--title") } : {}),
75
100
  ...(configuration === undefined ? {} : { configuration }),
@@ -114,8 +139,8 @@ async function getJob(args, options) {
114
139
  `Job ${job.id} (task ${job.taskId})`,
115
140
  ` status: ${job.status}`,
116
141
  ` owner: ${formatOwner(job.owner)}`,
117
- ` head: ${job.head}`,
118
- ` workspace: ${job.workspace}`,
142
+ ...(job.delegation ? [` delegated task: ${job.delegation.taskId}`, ` goal: ${job.delegation.goal}`]
143
+ : [` head: ${job.head}`, ` workspace: ${job.workspace}`]),
119
144
  ` created: ${job.createdAt}`
120
145
  ];
121
146
  if (job.startedAt !== undefined)
@@ -129,7 +154,7 @@ async function getJob(args, options) {
129
154
  if (job.result.unknownReason !== undefined)
130
155
  lines.push(` reason: ${job.result.unknownReason}`);
131
156
  }
132
- for (const step of job.steps) {
157
+ for (const step of job.steps ?? []) {
133
158
  lines.push(` step ${step.name}: ${step.command}`);
134
159
  }
135
160
  return `${lines.join("\n")}\n`;
@@ -194,6 +194,11 @@ function roleContext(args, store, options) {
194
194
  nativeTurnId: sessions.providerBinding.run.nativeTurnId,
195
195
  status: sessions.providerBinding.run.status
196
196
  },
197
+ ...(role.jobOwner === undefined ? {} : { job: {
198
+ owner: role.jobOwner,
199
+ read: `yui job show ${role.jobOwner.scope.kind === "global" ? "--global" : `--task ${role.jobOwner.scope.taskId}`} --job ${role.jobOwner.jobId} --json`,
200
+ boundary: "Job execution only. This Role is not Operator and owns no Task delivery authority. Read the exact Run from its original message."
201
+ } }),
197
202
  retry: providerRetrySummary(sessions?.providerBinding),
198
203
  // Historical interrupt receipts are available through Session inspection;
199
204
  // only this Turn's exact identity is required at entry.