@zq-silk/yui 2.23.0 → 2.25.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 (51) hide show
  1. package/README.md +9 -4
  2. package/dist/artifacts/artifactCapability.js +6 -6
  3. package/dist/artifacts/gitArtifactRef.js +10 -10
  4. package/dist/artifacts/jobArtifact.js +5 -5
  5. package/dist/artifacts/managedGit.js +15 -12
  6. package/dist/artifacts/taskArtifactRepository.js +35 -33
  7. package/dist/cli.js +9 -2
  8. package/dist/commands/configCommands.js +64 -1
  9. package/dist/commands/projectCommands.js +9 -9
  10. package/dist/commands/taskCompletionGate.js +1 -1
  11. package/dist/commands/taskIntegrationCommands.js +1 -1
  12. package/dist/commands/taskPublicationAdoptCommand.js +1 -1
  13. package/dist/commands/taskUpstreamCommands.js +1 -1
  14. package/dist/config/configCatalog.js +3 -0
  15. package/dist/config/gitConfig.js +20 -0
  16. package/dist/config/yuiConfig.js +1 -0
  17. package/dist/controlPlaneCli.js +4 -4
  18. package/dist/controller/jobArtifactControl.js +3 -3
  19. package/dist/controller/jobDefinitionControl.js +1 -1
  20. package/dist/integration/gitIntegrationService.js +1 -1
  21. package/dist/kernel/builtinCapabilities.js +3 -3
  22. package/dist/repository/gitCommand.js +153 -0
  23. package/dist/repository/gitCompatibility.js +3 -2
  24. package/dist/repository/gitWorkspace.js +579 -563
  25. package/dist/repository/gitWorktreeInventory.js +15 -19
  26. package/dist/repository/projectRefresh.js +1 -1
  27. package/dist/repository/taskBaseFreshness.js +1 -4
  28. package/dist/repository/taskWorkspacePreparer.js +1 -1
  29. package/dist/runtime/nativeExecutable.js +5 -2
  30. package/dist/runtime/platform.js +11 -0
  31. package/dist/storage/sqliteSchema.js +10 -0
  32. package/dist/storage/storageVersions.js +1 -1
  33. package/dist/storage/taskStore.js +7 -1
  34. package/dist/web/webConversation.js +2 -2
  35. package/dist/web/webMaterials.js +19 -19
  36. package/dist/web/webTaskSurface.js +3 -3
  37. package/dist/workspace/workItemChangeSetManager.js +3 -3
  38. package/docs/project-refresh.md +3 -2
  39. package/docs/project-refresh.zh-CN.md +2 -1
  40. package/docs/release-workflow.md +58 -2
  41. package/docs/release-workflow.zh-CN.md +45 -1
  42. package/docs/roles-and-configuration.md +34 -0
  43. package/docs/roles-and-configuration.zh-CN.md +27 -0
  44. package/docs/storage-baseline.md +5 -1
  45. package/docs/storage-baseline.zh-CN.md +3 -1
  46. package/docs/testing/verification-levels.md +7 -7
  47. package/docs/testing/verification-levels.zh-CN.md +6 -6
  48. package/i18n/README.zh-CN.md +8 -4
  49. package/package.json +1 -1
  50. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  51. package/dist/runtime/native/darwin-x64/process-identity +0 -0
@@ -1,16 +1,12 @@
1
- import { execFile } from "node:child_process";
2
1
  import { lstat, readFile, readdir, realpath } from "node:fs/promises";
3
2
  import { basename, dirname, join, resolve } from "node:path";
4
- import { promisify } from "node:util";
5
3
  import { gitOutputLine, supportedGit } from "./gitCompatibility.js";
6
- const execute = promisify(execFile);
7
- async function run(args) {
8
- return (await execute(supportedGit().path, args, {
9
- encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024
10
- })).stdout;
4
+ import { executeGitCommand } from "./gitCommand.js";
5
+ async function run(args, budget) {
6
+ return (await executeGitCommand(supportedGit(budget?.totalMs).path, args, { budget })).stdout.toString("utf8");
11
7
  }
12
- export async function gitPath(cwd, args) {
13
- return resolve(cwd, gitOutputLine(await run(["-C", cwd, "rev-parse", ...args])));
8
+ export async function gitPath(cwd, args, budget) {
9
+ return resolve(cwd, gitOutputLine(await run(["-C", cwd, "rev-parse", ...args], budget)));
14
10
  }
15
11
  async function exists(path) {
16
12
  try {
@@ -29,13 +25,13 @@ async function exists(path) {
29
25
  * never write/prune them, and never infer ownership merely from a directory.
30
26
  * Mutating consumers still check the recorded owner and ask Git to remove it.
31
27
  */
32
- export async function readGitWorktrees(cwd) {
33
- const common = await realpath(await gitPath(cwd, ["--git-common-dir"]));
28
+ export async function readGitWorktrees(cwd, budget) {
29
+ const common = await realpath(await gitPath(cwd, ["--git-common-dir"], budget));
34
30
  const result = [];
35
31
  // This is also Git's own main-worktree listing convention (including bare
36
32
  // and separate-git-dir repositories), not a claim of managed ownership.
37
33
  const main = basename(common) === ".git" ? dirname(common) : common;
38
- result.push(await inspect(common, main, false));
34
+ result.push(await inspect(common, main, false, budget));
39
35
  const directory = join(common, "worktrees");
40
36
  let entries;
41
37
  try {
@@ -59,27 +55,27 @@ export async function readGitWorktrees(cwd) {
59
55
  const path = dirname(resolve(admin, pointer));
60
56
  const adminCommon = await realpath(resolve(cwd, gitOutputLine(await run([
61
57
  "-C", cwd, `--git-dir=${admin}`, "rev-parse", "--git-common-dir"
62
- ]))));
58
+ ], budget))));
63
59
  if (adminCommon !== common)
64
60
  throw new Error(`Git worktree common directory mismatch: ${admin}; retained.`);
65
61
  if (await exists(path)) {
66
- const actual = await realpath(await gitPath(path, ["--git-dir"]));
62
+ const actual = await realpath(await gitPath(path, ["--git-dir"], budget));
67
63
  if (actual !== admin)
68
64
  throw new Error(`Git worktree back-pointer mismatch: ${path}; retained.`);
69
65
  }
70
- result.push(await inspect(admin, path, true));
66
+ result.push(await inspect(admin, path, true, budget));
71
67
  }
72
68
  if (new Set(result.map(entry => entry.path)).size !== result.length) {
73
69
  throw new Error("Duplicate Git worktree registrations; retained.");
74
70
  }
75
71
  return result;
76
72
  }
77
- async function inspect(admin, path, linked) {
73
+ async function inspect(admin, path, linked, budget) {
78
74
  // Ask Git rather than parsing HEAD: Git owns ref storage (including newer
79
75
  // backends), symbolic resolution and per-worktree pseudorefs.
80
76
  let branch;
81
77
  try {
82
- branch = gitOutputLine(await run([`--git-dir=${admin}`, "symbolic-ref", "--quiet", "HEAD"]));
78
+ branch = gitOutputLine(await run([`--git-dir=${admin}`, "symbolic-ref", "--quiet", "HEAD"], budget));
83
79
  }
84
80
  catch (error) {
85
81
  if (error.code !== 1)
@@ -90,7 +86,7 @@ async function inspect(admin, path, linked) {
90
86
  }
91
87
  let head;
92
88
  try {
93
- head = gitOutputLine(await run([`--git-dir=${admin}`, "rev-parse", "--verify", "HEAD"]));
89
+ head = gitOutputLine(await run([`--git-dir=${admin}`, "rev-parse", "--verify", "HEAD"], budget));
94
90
  if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(head))
95
91
  throw new Error("Invalid Git HEAD");
96
92
  }
@@ -99,7 +95,7 @@ async function inspect(admin, path, linked) {
99
95
  if (branch === undefined)
100
96
  throw error;
101
97
  try {
102
- await run([`--git-dir=${admin}`, "show-ref", "--verify", "--quiet", branch]);
98
+ await run([`--git-dir=${admin}`, "show-ref", "--verify", "--quiet", branch], budget);
103
99
  }
104
100
  catch (missing) {
105
101
  if (missing.code === 1) {
@@ -41,7 +41,7 @@ export async function refreshRegisteredProject(store, id, options = {}) {
41
41
  try {
42
42
  const current = check();
43
43
  options.beforeRefresh?.();
44
- const refreshed = await (options.git ?? new NodeGitWorkspace()).refresh({
44
+ const refreshed = await (options.git ?? new NodeGitWorkspace(() => store.getConfig())).refresh({
45
45
  repositoryPath: current.path, remoteUrl: current.remoteUrl, stableRef: current.stableBranch
46
46
  });
47
47
  return { project: current, ...refreshed };
@@ -59,7 +59,7 @@ export function recordTaskBaseProvenanceEvents(tx, taskId, provenance, now) {
59
59
  }
60
60
  export async function inspectTaskBaseFreshness(taskId, store, options = {}) {
61
61
  const task = requireTask(store, taskId);
62
- const git = options.git ?? newGitWorkspace();
62
+ const git = options.git ?? new NodeGitWorkspace(() => store.getConfig());
63
63
  const workspace = store.getTaskWorkspace(taskId);
64
64
  // Collect active AgentRun workspace snapshots once so every Project entry can
65
65
  // report whether a live AgentRun is pinned to a different commit.
@@ -169,9 +169,6 @@ export function renderTaskBaseFreshnessReport(report) {
169
169
  }
170
170
  return lines.join("\n").concat("\n");
171
171
  }
172
- function newGitWorkspace() {
173
- return new NodeGitWorkspace();
174
- }
175
172
  function requireTask(store, taskId) {
176
173
  const task = store.getTask(taskId);
177
174
  if (task === null)
@@ -67,7 +67,7 @@ export class FileTaskWorkspacePreparer {
67
67
  git;
68
68
  now;
69
69
  maintenanceLockOptions;
70
- constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), maintenanceLockOptions = {}) {
70
+ constructor(home, store, git = new NodeGitWorkspace(() => store.getConfig()), now = () => new Date(), maintenanceLockOptions = {}) {
71
71
  this.home = home;
72
72
  this.store = store;
73
73
  this.git = git;
@@ -1,9 +1,12 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { unsupportedPlatformMessage } from "./platform.js";
2
3
  /** Native binaries are shipped together; never execute another platform's helper. */
3
4
  export function nativeExecutable(name, platform = process.platform, architecture = process.arch) {
4
5
  const target = `${platform}-${architecture}`;
5
- if (!["linux-x64", "darwin-x64", "darwin-arm64"].includes(target)
6
- || (name === "process-identity" && platform !== "darwin")) {
6
+ const failure = unsupportedPlatformMessage(platform, architecture);
7
+ if (failure !== undefined)
8
+ throw new Error(failure);
9
+ if (name === "process-identity" && platform !== "darwin") {
7
10
  throw new Error(`Unsupported native runtime: ${target}/${name}.`);
8
11
  }
9
12
  return fileURLToPath(new URL(`./native/${target}/${name}`, import.meta.url));
@@ -0,0 +1,11 @@
1
+ /** Check the Node runtime's exact OS/architecture before loading the control plane. */
2
+ export function unsupportedPlatformMessage(platform = process.platform, architecture = process.arch) {
3
+ const target = `${platform}-${architecture}`;
4
+ if (target === "linux-x64" || target === "darwin-arm64")
5
+ return undefined;
6
+ return `Unsupported Yui runtime: ${target}. `
7
+ + "Yui supports Linux x64 (glibc) and macOS Apple Silicon with native arm64 Node.js."
8
+ + (target === "darwin-x64"
9
+ ? " macOS Intel is no longer supported; Intel Macs must remain on Yui 2.23.0."
10
+ : "");
11
+ }
@@ -315,6 +315,16 @@ const MINOR_UPGRADES = Object.freeze([{
315
315
  // Additive exact-identity intent/confirmation files under runtime/native-titles.
316
316
  // Never invent native ownership or replay history during migration.
317
317
  sql: ""
318
+ }, {
319
+ fromVersion: "1.28",
320
+ toVersion: "1.29",
321
+ name: "git-command-budgets",
322
+ introducedIn: "2.24.0",
323
+ sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
324
+ targetChecksum: CURRENT_SCHEMA_CHECKSUM,
325
+ // Optional local/remote Git budgets in config. Absence uses runtime defaults;
326
+ // preserve every existing payload byte rather than persisting defaults.
327
+ sql: ""
318
328
  }]);
319
329
  export function storageMinorUpgradePlan(from) {
320
330
  if (from === CURRENT_STORAGE_VERSION)
@@ -1,4 +1,4 @@
1
- export const CURRENT_STORAGE_VERSION = "1.28";
1
+ export const CURRENT_STORAGE_VERSION = "1.29";
2
2
  export const MIN_SUPPORTED_STORAGE_VERSION = "1.0";
3
3
  export const STORAGE_FORMAT = "yui-home";
4
4
  export function isStorageVersion(value) {
@@ -4,7 +4,7 @@ import { validateModelConnection } from "../models/modelManagementConfig.js";
4
4
  import { basename, dirname, join, resolve } from "node:path";
5
5
  import { isDeepStrictEqual } from "node:util";
6
6
  import { resolveTimeZone } from "../config/timeZone.js";
7
- import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
7
+ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
8
8
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
9
9
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
10
10
  import { validateReviewConfig } from "../review/reviewConfig.js";
@@ -131,6 +131,9 @@ export function validateYuiConfig(config) {
131
131
  "controllerTaskConcurrency",
132
132
  "agentLaunchInactivityTimeoutSeconds",
133
133
  "deliveryTimeoutSeconds",
134
+ "gitLocalTimeoutSeconds",
135
+ "gitRemoteInactivityTimeoutSeconds",
136
+ "gitRemoteTimeoutSeconds",
134
137
  "resourcesQuarantineTtlHours",
135
138
  "tmuxBin",
136
139
  "tmuxHistoryLimit",
@@ -159,6 +162,9 @@ export function validateYuiConfig(config) {
159
162
  resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
160
163
  resolveAgentLaunchInactivityTimeoutSeconds(config.agentLaunchInactivityTimeoutSeconds);
161
164
  resolveDeliveryTimeoutSeconds(config.deliveryTimeoutSeconds);
165
+ resolveGitLocalTimeoutSeconds(config.gitLocalTimeoutSeconds);
166
+ resolveGitRemoteInactivityTimeoutSeconds(config.gitRemoteInactivityTimeoutSeconds);
167
+ resolveGitRemoteTimeoutSeconds(config.gitRemoteTimeoutSeconds);
162
168
  resolveTmuxBin(config.tmuxBin);
163
169
  resolveTmuxHistoryLimit(config.tmuxHistoryLimit);
164
170
  resolveTelemetryEnabled(config.telemetryEnabled);
@@ -227,7 +227,7 @@ export function createWebConversationSurface(store, surface, plan, readPage = re
227
227
  if (["completed", "cancelled", "retired", "archived"].includes(task.status)) {
228
228
  throw new WebRequestRejected("Terminal Task materials are read-only.");
229
229
  }
230
- return saveTextMaterial(store.rootDirectory(), owner.taskId, id, input);
230
+ return saveTextMaterial(store.rootDirectory(), owner.taskId, id, input, () => store.getConfig());
231
231
  },
232
232
  state(owner, offset = 0) {
233
233
  const set = sessions(owner);
@@ -296,7 +296,7 @@ export function createWebConversationSurface(store, surface, plan, readPage = re
296
296
  if (owner.scope !== "task" || input.action === "interrupt")
297
297
  throw new WebRequestRejected("Materials require Task text input.");
298
298
  try {
299
- body = await materialInputBody(store.rootDirectory(), owner.taskId, required(body), input.materials);
299
+ body = await materialInputBody(store.rootDirectory(), owner.taskId, required(body), input.materials, () => store.getConfig());
300
300
  }
301
301
  catch (error) {
302
302
  throw new WebRequestRejected(error instanceof Error ? error.message : "Invalid material.");
@@ -16,24 +16,24 @@ const TEXT_EXTENSIONS = new Set([
16
16
  const TEXT_NAMES = new Set(["readme", "license", "dockerfile", "makefile"]);
17
17
  export const MAX_MATERIAL_BYTES = 256 * 1024;
18
18
  const PAGE_CHARACTERS = 12000;
19
- async function fixedImage(home, taskId, input) {
19
+ async function fixedImage(home, taskId, input, getConfig) {
20
20
  if (input.taskId !== taskId)
21
21
  throw new WebRequestRejected("Materials must belong to the same Task.");
22
22
  const ref = validateGitArtifactRef(input);
23
23
  if (ref.relativePath.length > 512)
24
24
  throw new WebRequestRejected("Image path is too long.");
25
- const file = await resolveGitArtifact(home, ref);
25
+ const file = await resolveGitArtifact(home, ref, getConfig);
26
26
  const metadata = rasterMetadata(file.bytes);
27
27
  return { ...ref, digest: file.digest, kind: "image", byteSize: file.bytes.length,
28
28
  ...metadata, base64: file.bytes.toString("base64") };
29
29
  }
30
30
  /** One selected immutable file, never a workspace path or a remote URL.
31
31
  * Markdown is bounded and whole so fences/tables are not split by pagination. */
32
- export async function readArtifactPreview(home, taskId, ref, offset = 0) {
32
+ export async function readArtifactPreview(home, taskId, ref, offset = 0, getConfig) {
33
33
  if (ref.taskId !== taskId)
34
34
  throw new WebRequestRejected("Materials must belong to the same Task.");
35
35
  const valid = validateGitArtifactRef(ref);
36
- const file = await resolveGitArtifact(home, valid);
36
+ const file = await resolveGitArtifact(home, valid, getConfig);
37
37
  return { ...valid, ...previewArtifactContent(file, offset) };
38
38
  }
39
39
  /** Shared data-only preview for Task and Job evidence; no path lookup or authority. */
@@ -81,12 +81,12 @@ function decodeText(bytes) {
81
81
  }
82
82
  return text;
83
83
  }
84
- async function fixedText(home, taskId, input) {
84
+ async function fixedText(home, taskId, input, getConfig) {
85
85
  if (input.taskId !== taskId)
86
86
  throw new WebRequestRejected("Materials must belong to the same Task.");
87
87
  const ref = validateGitArtifactRef(input);
88
88
  textPath(ref.relativePath);
89
- const file = await resolveGitArtifact(home, ref);
89
+ const file = await resolveGitArtifact(home, ref, getConfig);
90
90
  return { ref: { ...ref, digest: file.digest }, text: decodeText(file.bytes), bytes: file.bytes.length };
91
91
  }
92
92
  function page(text, offset = 0) {
@@ -100,26 +100,26 @@ function page(text, offset = 0) {
100
100
  return { content: text.slice(offset, end), offset, totalCharacters: text.length,
101
101
  nextOffset: end < text.length ? end : null };
102
102
  }
103
- export async function readTextArtifactPage(home, taskId, ref, offset = 0) {
104
- const file = await fixedText(home, taskId, ref);
103
+ export async function readTextArtifactPage(home, taskId, ref, offset = 0, getConfig) {
104
+ const file = await fixedText(home, taskId, ref, getConfig);
105
105
  return { ...file.ref, byteSize: file.bytes, ...page(file.text, offset) };
106
106
  }
107
- export async function compareTextArtifacts(home, taskId, before, after, offset = 0) {
108
- const left = await fixedText(home, taskId, before);
109
- const right = await fixedText(home, taskId, after);
107
+ export async function compareTextArtifacts(home, taskId, before, after, offset = 0, getConfig) {
108
+ const left = await fixedText(home, taskId, before, getConfig);
109
+ const right = await fixedText(home, taskId, after, getConfig);
110
110
  // Blob identities avoid interpreting user paths as options or reading the
111
111
  // working tree. Neither external diff drivers nor text converters can run.
112
- const repo = openTaskArtifactRepository(home, taskId);
112
+ const repo = openTaskArtifactRepository(home, taskId, getConfig);
113
113
  const diff = await managedGit(repo.repoPath, [
114
114
  "diff", "--no-ext-diff", "--no-textconv", "--no-color", "--unified=3",
115
115
  `${left.ref.commit}:${left.ref.relativePath}`, `${right.ref.commit}:${right.ref.relativePath}`, "--"
116
- ], { maxBuffer: 20 * 1024 * 1024, timeoutMs: 5000 });
116
+ ], { maxBuffer: 20 * 1024 * 1024, getConfig });
117
117
  return { taskId, before: left.ref, after: right.ref, ...page(diff, offset) };
118
118
  }
119
119
  /** Saves DATA only. Sending it is a separate explicit conversation action.
120
120
  * The normal artifact repository and commit message retain origin; no second
121
121
  * storage/receipt schema is introduced. Unknown saves are inspected, not replayed. */
122
- export async function saveTextMaterial(home, taskId, sessionId, input) {
122
+ export async function saveTextMaterial(home, taskId, sessionId, input, getConfig) {
123
123
  if (!/^[A-Za-z0-9_-]{1,128}$/u.test(input.requestId)
124
124
  || typeof input.name !== "string" || input.name.includes("/") || input.name.includes("\\")
125
125
  || typeof input.content !== "string")
@@ -130,7 +130,7 @@ export async function saveTextMaterial(home, taskId, sessionId, input) {
130
130
  throw new WebRequestRejected(`Material exceeds ${MAX_MATERIAL_BYTES} UTF-8 bytes or has invalid text.`);
131
131
  }
132
132
  decodeText(bytes);
133
- const repo = openTaskArtifactRepository(home, taskId);
133
+ const repo = openTaskArtifactRepository(home, taskId, getConfig);
134
134
  await repo.ensure();
135
135
  const head = (await repo.head());
136
136
  if ((await repo.list(head)).some(entry => entry.relativePath === relativePath)) {
@@ -141,22 +141,22 @@ export async function saveTextMaterial(home, taskId, sessionId, input) {
141
141
  return { taskId, relativePath, commit: head, digest: existing.digest };
142
142
  }
143
143
  return saveArtifactFile(home, taskId, { relativePath, bytes, expectedHead: head,
144
- message: `Text material from Leader Session ${sessionId}; request ${input.requestId}` });
144
+ message: `Text material from Leader Session ${sessionId}; request ${input.requestId}` }, getConfig);
145
145
  }
146
146
  /** Existing durable Message body carries self-contained frozen references.
147
147
  * Contents are deliberately absent: reading material cannot elevate it to an
148
148
  * instruction, execute it, or broaden the receiving Agent's Task authority. */
149
- export async function materialInputBody(home, taskId, body, refs) {
149
+ export async function materialInputBody(home, taskId, body, refs, getConfig) {
150
150
  if (!Array.isArray(refs) || refs.length > 8)
151
151
  throw new WebRequestRejected("Select at most eight material references.");
152
152
  const verified = [];
153
153
  for (const ref of refs) {
154
154
  if (isRasterArtifact(ref.relativePath)) {
155
- const image = await fixedImage(home, taskId, ref);
155
+ const image = await fixedImage(home, taskId, ref, getConfig);
156
156
  verified.push({ taskId, commit: image.commit, relativePath: image.relativePath, digest: image.digest });
157
157
  }
158
158
  else
159
- verified.push((await fixedText(home, taskId, ref)).ref);
159
+ verified.push((await fixedText(home, taskId, ref, getConfig)).ref);
160
160
  }
161
161
  if (!verified.length)
162
162
  return body;
@@ -136,7 +136,7 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
136
136
  artifacts: async (taskId, selectedCommit, offset = 0) => {
137
137
  if (store.getTask(taskId) === null)
138
138
  throw new WebRequestRejected("Task not found.");
139
- const repo = openTaskArtifactRepository(store.rootDirectory(), taskId);
139
+ const repo = openTaskArtifactRepository(store.rootDirectory(), taskId, () => store.getConfig());
140
140
  if (selectedCommit !== undefined && !repo.exists())
141
141
  throw new WebRequestRejected("Artifact revision unavailable.");
142
142
  const commit = selectedCommit ?? await repo.head();
@@ -151,12 +151,12 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
151
151
  throw new WebRequestRejected("Task not found.");
152
152
  // The browser always names a fixed revision. Never substitute HEAD after
153
153
  // a missing object, and never execute/render artifact HTML as an app.
154
- return readArtifactPreview(store.rootDirectory(), taskId, { taskId, relativePath, commit }, offset);
154
+ return readArtifactPreview(store.rootDirectory(), taskId, { taskId, relativePath, commit }, offset, () => store.getConfig());
155
155
  },
156
156
  artifactDiff: async (taskId, relativePath, commit, beforeCommit, offset = 0) => {
157
157
  if (store.getTask(taskId) === null)
158
158
  throw new WebRequestRejected("Task not found.");
159
- return compareTextArtifacts(store.rootDirectory(), taskId, { taskId, relativePath, commit: beforeCommit }, { taskId, relativePath, commit }, offset);
159
+ return compareTextArtifacts(store.rootDirectory(), taskId, { taskId, relativePath, commit: beforeCommit }, { taskId, relativePath, commit }, offset, () => store.getConfig());
160
160
  },
161
161
  globalState: (roleName) => {
162
162
  const role = store.getGlobalRole(roleName);
@@ -26,7 +26,7 @@ export class WorkItemChangeSetManager {
26
26
  const context = requireCapturableContext(this.store, taskId, workItemId, options.taskFinalReviewContract);
27
27
  const entries = capturableEntries(context);
28
28
  if (context.source === "task-main") {
29
- const git = new NodeGitWorkspace();
29
+ const git = new NodeGitWorkspace(() => this.store.getConfig());
30
30
  for (const { entry, expectedHead } of entries) {
31
31
  if (!await git.isClean(entry.path)) {
32
32
  throw new Error(`Exact direct Candidate Task main must be clean: ${context.workItemId}/${entry.projectId}.`);
@@ -64,7 +64,7 @@ export class WorkItemChangeSetManager {
64
64
  const workspace = this.store.getWorkItemWorkspace(item.taskId, item.id);
65
65
  if (workspace === null)
66
66
  return { proof: null, checks: [] };
67
- const git = new NodeGitWorkspace();
67
+ const git = new NodeGitWorkspace(() => this.store.getConfig());
68
68
  const projects = [];
69
69
  const checks = [];
70
70
  const sources = [`work-item:${taskId}/${workItemId}`,
@@ -189,7 +189,7 @@ export class WorkItemChangeSetManager {
189
189
  if (unresolved !== undefined) {
190
190
  throw new Error(`Task has an unresolved Integration Attempt: ${task.id}/${unresolved.id}.`);
191
191
  }
192
- const git = new NodeGitWorkspace();
192
+ const git = new NodeGitWorkspace(() => this.store.getConfig());
193
193
  const remoteHeads = new Map();
194
194
  const findPublishedCommit = async (entry, headCommit) => {
195
195
  const project = this.store.getProject(entry.projectId);
@@ -80,7 +80,8 @@ upstream is left untouched and carries no such promise.
80
80
 
81
81
  ## Scope of adoption
82
82
 
83
- This operation adds no persistent Yui schema or migration. Installing code
84
- that implements it does not itself refresh a real Project. A normal authorized
83
+ Refresh bookkeeping adds no persistent Yui schema. The shared remote Git
84
+ budget settings require storage 1.29; see [configuration](roles-and-configuration.md#git-operation-budgets).
85
+ Installing this code does not itself refresh a real Project. A normal authorized
85
86
  refresh applies the behavior; Task completion, integration, publication and
86
87
  archive remain separate operations.
@@ -63,5 +63,6 @@ upstream 保持原样,不作这一承诺。
63
63
 
64
64
  ## 采用边界
65
65
 
66
- 本操作不新增 Yui 持久 schema 或迁移。安装实现代码不等于刷新真实 Project;
66
+ refresh 记账不新增 Yui 持久 schema;共享远程 Git 预算配置要求存储 1.29,
67
+ 见[配置说明](roles-and-configuration.zh-CN.md#git-操作预算)。安装实现代码不等于刷新真实 Project;
67
68
  正常获授权的 refresh 才应用此行为。Task 完成、集成、发布和归档仍是独立操作。
@@ -51,6 +51,62 @@ merely to avoid a version decision. Package versions remain distinct from protoc
51
51
  and persistent-storage contracts: their compatibility checks and required migrations
52
52
  are unchanged, and the storage rules below do not authorize a package release.
53
53
 
54
+ ### 2.25.0 compatibility and migration notes
55
+
56
+ This minor release removes macOS Intel support. Supported runtimes are now
57
+ Linux x64 (glibc) and macOS Apple Silicon with native arm64 Node.js. macOS x64,
58
+ including x64 Node.js under Rosetta on Apple Silicon, is rejected before either
59
+ CLI product is loaded or the Home is opened. Intel Macs must remain on 2.23.0;
60
+ Apple Silicon users must switch to native arm64 Node.js before upgrading.
61
+ The combined npm manifest cannot express OS/CPU pairs, so npm installation can
62
+ still succeed on an unsupported combination. Runtime startup fails explicitly;
63
+ no install scripts, fallback helpers or first-use downloads are added.
64
+ The existing updater's staged-target preflight also rejects that runtime before
65
+ Controller shutdown, global binary activation or storage migration.
66
+
67
+ Native builds, package contents, Core CI and release smoke now cover only those
68
+ two platforms. Every supported Node 20/22/24/26 install remains gated (8 rows),
69
+ with process lifecycle coverage on Node 24 for each platform. The `v2.24.0`
70
+ tag is retained as unpublished evidence: required Intel smoke jobs did not
71
+ acquire hosted runners, and neither npm nor GitHub Release was published.
72
+ 2.25.0 includes its complete configurable Git budget/error-handling changes
73
+ described below.
74
+
75
+ Storage remains **1.29**; the platform removal adds no migration. Updating from
76
+ 2.23.0 still applies the declared **1.28 → 1.29** Git configuration migration.
77
+ Back up the Home and use the normal upgrade/migration path; do not open a
78
+ migrated Home with an older binary. Controller/Host protocols remain v1.
79
+ Publication does not update the global CLI, migrate production data or restart
80
+ shared services. Deterministic package/OS checks do not establish real-model
81
+ or real remote-server behavior.
82
+
83
+ ### 2.24.0 unpublished tag: Git budgets and migration
84
+
85
+ Git workspace and artifact operations now use a configurable local total
86
+ budget, independent of clone, remote fetch and network `ls-remote`. Configure
87
+ `git-local-timeout-seconds` (default 30),
88
+ `git-remote-inactivity-timeout-seconds` (default 120), and
89
+ `git-remote-timeout-seconds` (default 1800) under `yui config tools` or Web
90
+ settings. Each accepts integer seconds from 1 to 86400. Changes apply to the
91
+ next command, including Controller-owned commands; in-flight budgets do not
92
+ change. See [Git operation budgets](roles-and-configuration.md#git-operation-budgets).
93
+
94
+ Clone/fetch request progress without a terminal. Output resets only the
95
+ inactivity clock, never the total deadline; activity is not semantic progress
96
+ proof. Remote stderr keeps a bounded tail. Timeout errors retain their kind
97
+ and budget, elapsed time, exit code, signal and stderr instead of reporting
98
+ only the last progress line. Deadline termination includes the invocation's
99
+ owned transport helpers; no automatic Git retry or rollback is introduced.
100
+
101
+ Storage advances **1.28 → 1.29** through the centralized additive migration
102
+ for the three optional settings. Existing config bytes remain unchanged and
103
+ missing values use runtime defaults. Back up the Home and use the normal
104
+ upgrade/migration path; do not open a migrated Home with an older binary.
105
+ Controller/Host protocols remain v1. Exact-merge Core CI and the same runtime
106
+ archive's fresh-install matrix gate publication. These deterministic checks
107
+ do not establish real remote-server or real-model behavior. Publication does
108
+ not update the global CLI, migrate production data or restart shared services.
109
+
54
110
  ### 2.23.0 compatibility and migration notes
55
111
 
56
112
  Host upgrades use explicit lifecycle signals and safe drain/reattachment
@@ -766,7 +822,7 @@ protection and valid exact-identity caches remain normal runtime behavior.
766
822
 
767
823
  Stable releases use npm `latest`; prereleases use `next`, never the stable
768
824
  default. The release workflow builds one runtime archive, verifies it across
769
- supported Node versions on Linux x64, macOS x64 and macOS arm64, publishes that exact archive to npm, then creates the
825
+ supported Node versions on Linux x64 and macOS arm64, publishes that exact archive to npm, then creates the
770
826
  matching GitHub Release. Every release carries the runtime archive, checksum and
771
827
  provenance. A stable tag becomes GitHub latest; a prerelease does not.
772
828
 
@@ -1091,7 +1147,7 @@ It reuses the exact commit that passed core CI and adds only artifact assembly,
1091
1147
  fresh installation, and provenance checks required to publish.
1092
1148
 
1093
1149
  Business contracts and full Jobs/Schedules wiring run once in Core CI on Linux
1094
- Node 24. Release still installs the same checksummed archive on all three
1150
+ Node 24. Release still installs the same checksummed archive on both supported
1095
1151
  platforms × Node 20/22/24/26: every install checks CLI/setup/native helpers,
1096
1152
  Doctor/PTY and real SQLite SQL/GC. Node 24 additionally checks real process
1097
1153
  lifecycle on each platform. See the [coverage table](testing/verification-levels.md#ci-and-release).
@@ -36,6 +36,50 @@ Agent 选择一个预先声明的计划,设施从持久状态驱动该计划
36
36
  不能将不兼容冒称为兼容,也不为回避版本选择新增历史兼容机制。包版本与协议、
37
37
  持久存储契约仍然独立:兼容性检查和必要迁移要求不变,下文存储规则不授予发版权限。
38
38
 
39
+ ### 2.25.0 兼容性及迁移说明
40
+
41
+ 本次 minor 版本移除 macOS Intel 支持。仅支持 Linux x64(glibc)和使用原生
42
+ arm64 Node.js 的 macOS Apple Silicon。macOS x64(包括 Apple Silicon 上通过
43
+ Rosetta 运行的 x64 Node.js)会在加载任一 CLI 产品或打开 Home 前明确拒绝。
44
+ Intel Mac 用户须停留在 2.23.0;Apple Silicon 用户须先切换到原生 arm64 Node.js。
45
+ 合并 npm 包的 OS/CPU 字段不能表达平台配对,因此不支持的组合仍可能安装成功,
46
+ 但启动时会明确失败;不新增安装脚本、备用 helper 或首次使用下载。
47
+ 既有 updater 的目标包预检也会在停止 Controller、替换全局二进制或迁移存储前拒绝。
48
+
49
+ 原生构建、包内容、Core CI 和发布冒烟同步收敛到上述两个平台。
50
+ 保留全部受支持 Node 20/22/24/26 的全新安装门禁(8 组),各平台 Node 24
51
+ 仍验证真实进程生命周期。保留 `v2.24.0` tag 作为未发布证据:
52
+ 必需的 Intel 冒烟作业未能获得 hosted runner,npm 和 GitHub Release 均未发布。
53
+ 2.25.0 包含下文完整的 Git 预算配置及错误保真变更。
54
+
55
+ 存储仍为 **1.29**,移除平台本身不新增迁移;从 2.23.0 升级仍执行已声明的
56
+ Git 配置 **1.28 → 1.29** 迁移。升级前备份 Home,使用正常升级/迁移路径,
57
+ 不要用旧二进制打开已迁移的 Home。Controller/Host 协议仍为 v1。
58
+ 发布本身不更新全局 CLI、不迁移生产数据,也不重启共享服务。
59
+ 确定性安装包及 OS 检查不证明真实模型或真实远端服务器行为。
60
+
61
+ ### 2.24.0 未发布 tag:Git 预算及迁移
62
+
63
+ Git 工作区和产物操作使用可配置的本地总耗时预算,与 clone、远程 fetch 和需要连接
64
+ 远端的 `ls-remote` 分离。在 `yui config tools` 或 Web 设置中配置
65
+ `git-local-timeout-seconds`(默认 30)、
66
+ `git-remote-inactivity-timeout-seconds`(默认 120)及
67
+ `git-remote-timeout-seconds`(默认 1800);三项均接受 1–86400 的整数秒。
68
+ 保存后下一条命令生效,包含 Controller 发起的命令,已启动命令保持原预算。
69
+ 参见 [Git 操作预算](roles-and-configuration.zh-CN.md#git-操作预算)。
70
+
71
+ clone/fetch 无终端时也请求进度。输出只重置无进展计时,不延长总耗时上限;
72
+ 有输出不等于传输实际推进。远程 stderr 保留有界尾部。错误保留超时类型和预算、
73
+ 实际耗时、退出码、信号及 stderr,不再只显示最后一行进度。预算耗尽时终止本次调用
74
+ 自己的传输 helper;不引入自动 Git 重试或回滚。
75
+
76
+ 存储通过集中式增量迁移从 **1.28 → 1.29**,声明三项可选配置。
77
+ 既有配置逐字节保留,缺失字段采用运行时默认值。升级前备份 Home,使用正常升级/迁移
78
+ 路径,不要用旧二进制打开已迁移的 Home。Controller/Host 协议仍为 v1。
79
+ 发布前要求确切合并提交的 Core CI 及同一 runtime 包的全新安装矩阵通过;
80
+ 确定性检查不证明真实远端服务器或真实模型行为。发布本身不更新全局 CLI、
81
+ 不迁移生产数据,也不重启共享服务。
82
+
39
83
  ### 2.23.0 兼容性及迁移说明
40
84
 
41
85
  Host 升级使用明确的生命周期信号及安全排空/重新挂接边界,不重放业务输入、
@@ -821,7 +865,7 @@ yui task grant revoke task-15 capability-grant-1
821
865
  确切 commit,只增加发布所需的产物装配、全新安装和 provenance 检查。
822
866
 
823
867
  业务契约与完整 Jobs/Schedules 链路仅在 Core CI 的 Linux Node 24 执行一次。
824
- 发布仍在三平台 × Node 20/22/24/26 全部安装同一个已校验 tarball:每组都验证
868
+ 发布仍在两个受支持平台 × Node 20/22/24/26 全部安装同一个已校验 tarball:每组都验证
825
869
  CLI/setup/原生 helper、Doctor/PTY 和真实 SQLite SQL/GC;各平台 Node 24 额外验证
826
870
  真实进程生命周期。参见[分层覆盖表](testing/verification-levels.zh-CN.md#ci-与发布)。
827
871
  依赖安装不再隐含构建,每个源码作业只构建一次,再验证原生依赖;smoke 输出分段耗时。
@@ -18,6 +18,40 @@ requiring a same-named global Role. A global Role is a creation template only
18
18
  when the requested Task Role does not exist. Availability, producer separation
19
19
  and the frozen review candidate still apply.
20
20
 
21
+ ## Git operation budgets
22
+
23
+ All three Git timeout budgets have durable settings. Local commands (including
24
+ local ref copies and artifact repositories) have a total limit; clone, remote
25
+ fetch and network `ls-remote` have independent inactivity and total limits:
26
+
27
+ ```sh
28
+ yui config tools show
29
+ yui config tools set git-local-timeout-seconds 30
30
+ yui config tools set git-remote-inactivity-timeout-seconds 120
31
+ yui config tools set git-remote-timeout-seconds 1800
32
+ yui config tools clear git-local-timeout-seconds
33
+ yui config tools clear git-remote-inactivity-timeout-seconds
34
+ yui config tools clear git-remote-timeout-seconds
35
+ ```
36
+
37
+ All three accept integer seconds from 1 to 86400. Defaults are 30 seconds total
38
+ per local command, 120 seconds without stdout/stderr output and 1800 seconds
39
+ total per remote command.
40
+ They apply to the next command, including Controller-owned Project refresh
41
+ and Task preparation, without a restart; an in-flight command keeps its budgets.
42
+ Clone/fetch explicitly request progress even without a terminal. Each output
43
+ chunk resets only the silence timer, not the total deadline. Output is evidence
44
+ of activity, not proof that a transfer is advancing; the total limit still wins.
45
+ Remote stderr retains its last 64 KiB so progress cannot exhaust the output
46
+ buffer; stdout and local command output retain their existing buffer limits.
47
+
48
+ At either hard deadline Yui kills that invocation's process group, including
49
+ transport helpers, and waits for process/pipe closure. Errors preserve the
50
+ timeout kind/budget, elapsed time, exit code, signal and stderr where available.
51
+ No Git operation is automatically retried, and a failure is not rollback proof.
52
+ Storage 1.28 → 1.29 admits these optional configuration fields without
53
+ rewriting historical configuration or persisting default values.
54
+
21
55
  ## Optional instance codes and Session titles
22
56
 
23
57
  Each Role can independently set a display-only instance code. For example,