@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
@@ -20,6 +20,9 @@ export const CONFIG_DEFINITIONS = Object.freeze([
20
20
  { key: "resources-quarantine-ttl-hours", domain: "resources", property: "resourcesQuarantineTtlHours", label: "Resource quarantine TTL", summary: "Default observation window before purge, 1-720 hours (default: 24).", takesEffect: "The next resource GC command without an explicit TTL override." },
21
21
  { key: "tmux-bin", domain: "tools", property: "tmuxBin", label: "Tmux bin", summary: "Command or path to the tmux binary (default: tmux).", takesEffect: "The next CLI-owned tmux invocation; restart the Controller for Controller-owned launches." },
22
22
  { key: "tmux-history-limit", domain: "tools", property: "tmuxHistoryLimit", label: "Tmux history limit", summary: "History lines retained for newly created tmux sessions, 1000-1000000 (default: 100000).", takesEffect: "New tmux sessions; existing sessions report drift until recreated." },
23
+ { key: "git-local-timeout-seconds", domain: "tools", property: "gitLocalTimeoutSeconds", label: "Git local total timeout", summary: "Total elapsed-time limit for each local Git command, 1-86400 seconds (default: 30). Independent of remote transport budgets.", takesEffect: "The next local Git command, including Controller-owned commands; in-flight commands keep their budgets." },
24
+ { key: "git-remote-inactivity-timeout-seconds", domain: "tools", property: "gitRemoteInactivityTimeoutSeconds", label: "Git remote inactivity timeout", summary: "Maximum silence on stdout/stderr for clone, remote fetch and ls-remote, 1-86400 seconds (default: 120). Local Git uses its separate configurable total budget.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
25
+ { key: "git-remote-timeout-seconds", domain: "tools", property: "gitRemoteTimeoutSeconds", label: "Git remote total timeout", summary: "Total elapsed-time limit for each clone, remote fetch or ls-remote, 1-86400 seconds (default: 1800), even while progress continues.", takesEffect: "The next remote Git command, including Controller-owned commands; in-flight commands keep their budgets." },
23
26
  { key: "telemetry-enabled", domain: "tools", property: "telemetryEnabled", label: "Diagnostic telemetry", summary: "Enable the optional SQLite diagnostic telemetry projection (default: false); requires a SQLite Home.", takesEffect: "Standalone telemetry commands immediately; scheduler writes after Controller restart." },
24
27
  { key: "telemetry-terminal-keep", domain: "tools", property: "telemetryTerminalKeep", label: "Telemetry terminal keep", summary: "Positive-integer rows retained per terminal AgentRun (default: 200).", takesEffect: "Explicit retention immediately; automatic retention after the Controller restarts." },
25
28
  { key: "telemetry-run-cap", domain: "tools", property: "telemetryRunCap", label: "Telemetry AgentRun cap", summary: `Positive-integer maximum rows retained per AgentRun, at most ${MAX_RUN_CAP} (default: 50000).`, takesEffect: "Explicit retention immediately; automatic write-time capping after the Controller restarts." }
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS = 30;
2
+ export const DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS = 120;
3
+ export const DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS = 1_800;
4
+ export function resolveGitLocalTimeoutSeconds(value) {
5
+ return timeoutSeconds(value, DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, "gitLocalTimeoutSeconds");
6
+ }
7
+ export function resolveGitRemoteInactivityTimeoutSeconds(value) {
8
+ return timeoutSeconds(value, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, "gitRemoteInactivityTimeoutSeconds");
9
+ }
10
+ export function resolveGitRemoteTimeoutSeconds(value) {
11
+ return timeoutSeconds(value, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, "gitRemoteTimeoutSeconds");
12
+ }
13
+ function timeoutSeconds(value, fallback, label) {
14
+ if (value === undefined)
15
+ return fallback;
16
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > 86_400) {
17
+ throw new TypeError(`${label} must be an integer from 1 to 86400.`);
18
+ }
19
+ return value;
20
+ }
@@ -91,6 +91,7 @@ export const MAX_CONTROLLER_TASK_CONCURRENCY = 32;
91
91
  export const DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS = 300;
92
92
  export const DEFAULT_DELIVERY_TIMEOUT_SECONDS = 120;
93
93
  export const DEFAULT_TMUX_HISTORY_LIMIT = 100_000;
94
+ export { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_GIT_REMOTE_TIMEOUT_SECONDS, resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds } from "./gitConfig.js";
94
95
  export function resolveRuntimeHealth(configured) {
95
96
  if (configured === undefined || configured === null)
96
97
  return DEFAULT_RUNTIME_HEALTH_POLICY;
@@ -1111,7 +1111,7 @@ export async function main() {
1111
1111
  if (action === "list") {
1112
1112
  if (resolved.length !== 4)
1113
1113
  throw usageError(usage);
1114
- data = await listArtifactsCapability(home, taskId);
1114
+ data = await listArtifactsCapability(home, taskId, () => store.getConfig());
1115
1115
  }
1116
1116
  else if (action === "read") {
1117
1117
  const relativePath = resolved[4];
@@ -1120,7 +1120,7 @@ export async function main() {
1120
1120
  throw usageError(usage);
1121
1121
  data = await readArtifactCapability(home, taskId, {
1122
1122
  relativePath, ...(commit === undefined ? {} : { commit })
1123
- });
1123
+ }, () => store.getConfig());
1124
1124
  }
1125
1125
  else {
1126
1126
  // save: a delivery-authoritative action; a managed Task caller must be the current Leader.
@@ -1156,7 +1156,7 @@ export async function main() {
1156
1156
  relativePath, content, ...(encoding === undefined ? {} : { encoding }),
1157
1157
  ...(message === undefined ? {} : { message }),
1158
1158
  ...(expectedHead === undefined ? {} : { expectedHead })
1159
- });
1159
+ }, () => store.getConfig());
1160
1160
  }
1161
1161
  emit(JSON.stringify(data, null, 2), false, data);
1162
1162
  return;
@@ -2515,7 +2515,7 @@ async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReview
2515
2515
  repositoryPaths,
2516
2516
  previousRound: previous,
2517
2517
  candidate: actualTaskReviewCandidate,
2518
- git: new NodeGitWorkspace()
2518
+ git: new NodeGitWorkspace(() => store.getConfig())
2519
2519
  });
2520
2520
  if (assessment.kind === "ineligible") {
2521
2521
  throw usageError(`Delta-recheck is technically unavailable: ${assessment.reason}`);
@@ -19,7 +19,7 @@ export async function controlJobArtifact(store, home, value, now) {
19
19
  op: "run-get", scope: input.scope, caller: input.caller, id: input.id, runId: input.runId
20
20
  }, now);
21
21
  const run = current(); // Both definition and historical Assignment authorization.
22
- const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run));
22
+ const repo = openJobArtifactRepository(home, jobArtifactOwner(run), runReference(run), () => store.getConfig());
23
23
  if (input.op === "artifact-list") {
24
24
  const head = await repo.head();
25
25
  const files = head === null ? [] : await repo.list(head);
@@ -45,7 +45,7 @@ export async function controlJobArtifact(store, home, value, now) {
45
45
  if (!(run.artifacts ?? []).some(item => isDeepStrictEqual(item, ref))) {
46
46
  throw new Error("Artifact reference is not registered for this exact Run.");
47
47
  }
48
- const content = await readJobArtifactReference(home, ref);
48
+ const content = await readJobArtifactReference(home, ref, () => store.getConfig());
49
49
  current(); // A replaced Session during I/O must not receive the material.
50
50
  return boundedJobArtifactPayload({ reference: ref, ...(input.op === "artifact-preview"
51
51
  ? previewArtifactContent(content, input.offset) : presentArtifactContent(content)) }, "Artifact response", MAX_JOB_ARTIFACT_RESPONSE_BYTES);
@@ -79,7 +79,7 @@ export async function controlJobArtifact(store, home, value, now) {
79
79
  }
80
80
  assertCallerAuthorized(store, input.caller, ref.owner.taskId);
81
81
  }
82
- await readJobArtifactReference(home, ref);
82
+ await readJobArtifactReference(home, ref, () => store.getConfig());
83
83
  }
84
84
  try {
85
85
  return store.transaction(tx => {
@@ -303,7 +303,7 @@ export function authorizeGlobalJobStart(store, run) {
303
303
  const definition = store.getJobDefinition({ kind: "global" }, run.definitionId);
304
304
  if (definition?.builtin)
305
305
  requireProjectSyncTarget(store, definition);
306
- const project = requireRefreshProject({ listProjects: () => projects, rootDirectory: () => home }, run.projectId, true);
306
+ const project = requireRefreshProject({ listProjects: () => projects }, run.projectId, true);
307
307
  if (projectRefreshTargetDigest(project) !== projectRefreshDigest({
308
308
  kind: "builtin", action: run.action, projectId: run.projectId, input: run.actionInput
309
309
  }))
@@ -64,7 +64,7 @@ export class GitIntegrationService {
64
64
  environment;
65
65
  runtimeIsolation;
66
66
  #resourceRegistrarValue;
67
- constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
67
+ constructor(home, store, git = new NodeGitWorkspace(() => store.getConfig()), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId), jobPort) {
68
68
  this.store = store;
69
69
  this.git = git;
70
70
  this.now = now;
@@ -324,14 +324,14 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
324
324
  ...(params.encoding === undefined ? {} : { encoding: params.encoding }),
325
325
  ...(params.message === undefined ? {} : { message: params.message }),
326
326
  ...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
327
- });
327
+ }, () => store.getConfig());
328
328
  if (name === "artifact.read")
329
329
  return readArtifactCapability(store.rootDirectory(), taskId, {
330
330
  relativePath: params.relativePath,
331
331
  ...(params.commit === undefined ? {} : { commit: params.commit })
332
- });
332
+ }, () => store.getConfig());
333
333
  if (name === "artifact.list")
334
- return listArtifactsCapability(store.rootDirectory(), taskId);
334
+ return listArtifactsCapability(store.rootDirectory(), taskId, () => store.getConfig());
335
335
  if (name === "environment.prepare")
336
336
  return resources.prepare(taskId, params.plan);
337
337
  if (name === "environment.adopt")
@@ -0,0 +1,153 @@
1
+ import { spawn } from "node:child_process";
2
+ import { performance } from "node:perf_hooks";
3
+ import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
4
+ export const LOCAL_GIT_TIMEOUT_MS = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000;
5
+ /** Retain process facts independently of Git's last progress/error output. */
6
+ export class GitCommandError extends Error {
7
+ timeoutKind;
8
+ budget;
9
+ elapsedMs;
10
+ code;
11
+ exitCode;
12
+ signal;
13
+ stdout;
14
+ stderr;
15
+ stderrTruncated;
16
+ constructor(cause, timeoutKind, budget, elapsedMs, output) {
17
+ const failure = cause;
18
+ const code = failure?.code ?? output.exitCode;
19
+ const exitCode = output.exitCode;
20
+ const stderr = output.stderr.toString("utf8").trim();
21
+ const reasons = [
22
+ ...(timeoutKind === undefined ? [] : [
23
+ `${timeoutKind === "inactivity" ? "no-progress timeout" : "total timeout"} after `
24
+ + `${timeoutKind === "inactivity" ? budget.inactivityMs : budget.totalMs} ms`
25
+ ]),
26
+ ...(exitCode === null ? [] : [`exit code ${exitCode}`]),
27
+ ...(output.signal === null ? [] : [`signal ${output.signal}`]),
28
+ ...(typeof code === "string" ? [`${code}: ${failure?.message ?? "process error"}`] : [])
29
+ ];
30
+ if (reasons.length === 0)
31
+ reasons.push(failure?.message ?? "unknown process failure");
32
+ super(`Git command failed (${reasons.join("; ")}; elapsed ${elapsedMs} ms)`
33
+ + (stderr.length === 0 ? "." : `: ${output.stderrTruncated ? "[stderr tail; earlier output omitted] " : ""}${stderr}`), { cause });
34
+ this.timeoutKind = timeoutKind;
35
+ this.budget = budget;
36
+ this.elapsedMs = elapsedMs;
37
+ this.name = "GitCommandError";
38
+ this.code = code;
39
+ this.exitCode = exitCode;
40
+ this.signal = output.signal;
41
+ this.stdout = output.stdout;
42
+ this.stderr = stderr;
43
+ this.stderrTruncated = output.stderrTruncated;
44
+ }
45
+ }
46
+ /**
47
+ * One buffered invocation, never retried. Output refreshes only the inactivity
48
+ * clock; the total deadline never moves. At the hard deadline, kill the owned
49
+ * process group (including transport helpers) before reporting completion.
50
+ */
51
+ export async function executeGitCommand(program, args, options = {}) {
52
+ const budget = options.budget ?? { totalMs: LOCAL_GIT_TIMEOUT_MS };
53
+ for (const value of [budget.totalMs, ...(budget.inactivityMs === undefined ? [] : [budget.inactivityMs])]) {
54
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2_147_483_647) {
55
+ throw new TypeError("Git timeout budgets must be positive integer milliseconds within the timer range.");
56
+ }
57
+ }
58
+ const started = performance.now();
59
+ const child = spawn(program, args, {
60
+ ...(options.environment === undefined ? {} : { env: options.environment }),
61
+ detached: process.platform !== "win32",
62
+ windowsHide: true
63
+ });
64
+ const maxBuffer = options.maxBuffer ?? 1024 * 1024;
65
+ const chunks = { stdout: [], stderr: [] };
66
+ const sizes = { stdout: 0, stderr: 0 };
67
+ let stderrTruncated = false;
68
+ let failure;
69
+ let timeoutKind;
70
+ let terminationError;
71
+ const terminate = () => {
72
+ try {
73
+ if (child.pid !== undefined && process.platform !== "win32") {
74
+ try {
75
+ process.kill(-child.pid, "SIGKILL");
76
+ }
77
+ catch (error) {
78
+ if (error.code !== "ESRCH")
79
+ throw error;
80
+ }
81
+ }
82
+ else {
83
+ child.kill("SIGKILL");
84
+ }
85
+ }
86
+ catch (error) {
87
+ terminationError = error;
88
+ child.kill("SIGKILL");
89
+ }
90
+ };
91
+ const expire = (kind) => {
92
+ if (timeoutKind !== undefined || failure !== undefined)
93
+ return;
94
+ timeoutKind = kind;
95
+ terminate();
96
+ };
97
+ child.once("error", error => { failure ??= error; });
98
+ const completion = new Promise(settle => {
99
+ child.once("close", (exitCode, signal) => settle({
100
+ // A negative close code is an OS spawn error, not a process exit.
101
+ exitCode: exitCode !== null && exitCode >= 0 ? exitCode : null, signal
102
+ }));
103
+ });
104
+ const totalTimer = setTimeout(() => expire("total"), budget.totalMs);
105
+ const inactivityTimer = budget.inactivityMs === undefined
106
+ ? undefined : setTimeout(() => expire("inactivity"), budget.inactivityMs);
107
+ const collect = (stream, chunk) => {
108
+ if (chunk.length === 0)
109
+ return;
110
+ if (timeoutKind === undefined && failure === undefined)
111
+ inactivityTimer?.refresh();
112
+ if (stream === "stderr" && options.stderrTail) {
113
+ const limit = Math.min(maxBuffer, 64 * 1024);
114
+ stderrTruncated ||= sizes.stderr + chunk.length > limit;
115
+ const tail = Buffer.concat([...chunks.stderr, chunk]);
116
+ chunks.stderr = [tail.subarray(Math.max(0, tail.length - limit))];
117
+ sizes.stderr = Math.min(limit, tail.length);
118
+ return;
119
+ }
120
+ const remaining = maxBuffer - sizes[stream];
121
+ if (remaining > 0) {
122
+ chunks[stream].push(chunk.subarray(0, remaining));
123
+ sizes[stream] += Math.min(chunk.length, remaining);
124
+ }
125
+ if (chunk.length > remaining && failure === undefined) {
126
+ failure = Object.assign(new Error(`${stream} maxBuffer length exceeded (${maxBuffer} bytes).`), { code: "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" });
127
+ terminate();
128
+ }
129
+ };
130
+ child.stdout.on("data", chunk => collect("stdout", chunk));
131
+ child.stderr.on("data", chunk => collect("stderr", chunk));
132
+ child.stdin.on("error", error => {
133
+ failure ??= error;
134
+ terminate();
135
+ });
136
+ try {
137
+ child.stdin.end(options.input);
138
+ const status = await completion;
139
+ const result = { stdout: Buffer.concat(chunks.stdout), stderr: Buffer.concat(chunks.stderr), stderrTruncated };
140
+ if (failure !== undefined || timeoutKind !== undefined || status.exitCode !== 0 || status.signal !== null) {
141
+ const error = new GitCommandError(failure, timeoutKind, budget, Math.round(performance.now() - started), { ...result, ...status });
142
+ if (terminationError !== undefined) {
143
+ throw new AggregateError([error, terminationError], `${error.message} Process-group termination failed; helper effects are unconfirmed.`, { cause: error });
144
+ }
145
+ throw error;
146
+ }
147
+ return result;
148
+ }
149
+ finally {
150
+ clearTimeout(totalTimer);
151
+ clearTimeout(inactivityTimer);
152
+ }
153
+ }
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { realpathSync, statSync } from "node:fs";
3
3
  import { resolveExecutable } from "../external/pinnedCommandRunner.js";
4
+ import { DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS } from "../config/gitConfig.js";
4
5
  // The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
5
6
  // implement it by saving/restoring FETCH_HEAD without racing another writer.
6
7
  export const MINIMUM_GIT_VERSION = "2.29.0";
@@ -17,7 +18,7 @@ export function gitVersionSupport(output) {
17
18
  }
18
19
  let cached;
19
20
  /** Pin the executable checked, reusing only an unchanged on-disk identity. */
20
- export function supportedGit() {
21
+ export function supportedGit(timeoutMs = DEFAULT_GIT_LOCAL_TIMEOUT_SECONDS * 1_000) {
21
22
  const selected = resolveExecutable("git", process.env.PATH);
22
23
  if (selected === undefined)
23
24
  throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
@@ -27,7 +28,7 @@ export function supportedGit() {
27
28
  if (cached?.identity === identity)
28
29
  return cached;
29
30
  const output = execFileSync(path, ["--version"], {
30
- encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"]
31
+ encoding: "utf8", timeout: timeoutMs, stdio: ["ignore", "pipe", "pipe"]
31
32
  });
32
33
  const support = gitVersionSupport(output);
33
34
  if (!support.supported) {