@zq-silk/yui 2.3.0 → 2.4.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 (183) hide show
  1. package/README.md +15 -1
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +17 -8
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +25 -2
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +10 -1
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  23. package/dist/controller/jobControl.js +2 -0
  24. package/dist/controller/nativeHostInput.js +37 -0
  25. package/dist/controller/runtime.js +93 -2
  26. package/dist/controller/runtimeEventInbox.js +2 -2
  27. package/dist/controller/runtimeEventProcessor.js +3 -3
  28. package/dist/doctor/doctor.js +10 -0
  29. package/dist/execution/reviewMainRun.js +1 -0
  30. package/dist/execution/workItemMainRun.js +1 -0
  31. package/dist/executor/codexConfigConflict.js +6 -4
  32. package/dist/executor/effectiveLaunch.js +24 -1
  33. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  34. package/dist/integration/gitIntegrationService.js +7 -12
  35. package/dist/integration/integrationSourceApplication.js +4 -2
  36. package/dist/job/httpPreview.js +61 -0
  37. package/dist/job/jobRunner.js +12 -2
  38. package/dist/kernel/builtinCapabilities.js +3 -1
  39. package/dist/message/message.js +15 -1
  40. package/dist/output/rolePresentation.js +4 -1
  41. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  42. package/dist/repository/gitCompatibility.js +62 -0
  43. package/dist/repository/gitWorkspace.js +48 -71
  44. package/dist/repository/gitWorktreeInventory.js +121 -0
  45. package/dist/repository/project.js +74 -25
  46. package/dist/resources/resourceDiscovery.js +2 -2
  47. package/dist/resources/resourceGc.js +7 -4
  48. package/dist/resources/resourceRegistrar.js +2 -1
  49. package/dist/runtime/agentEndpoint.js +8 -0
  50. package/dist/runtime/agentHost.js +170 -3
  51. package/dist/runtime/codexAppServerRuntime.js +1 -1
  52. package/dist/runtime/codexNativeAccess.js +175 -0
  53. package/dist/runtime/controlledCodexTui.js +116 -0
  54. package/dist/runtime/publicReply.js +23 -0
  55. package/dist/runtime/runtimeObservation.js +48 -0
  56. package/dist/runtime/structuredProviderHost.js +16 -7
  57. package/dist/runtime/taskCostMetrics.js +133 -0
  58. package/dist/runtime/taskUsageMetrics.js +11 -2
  59. package/dist/runtime/taskUsageQuery.js +38 -0
  60. package/dist/scheduler/taskExecutionProjection.js +2 -0
  61. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  62. package/dist/storage/homeLayout.js +13 -0
  63. package/dist/storage/sqliteSchema.js +44 -0
  64. package/dist/storage/sqliteStore.js +53 -0
  65. package/dist/storage/storageVersions.js +1 -1
  66. package/dist/storage/storeRpc.js +2 -0
  67. package/dist/storage/taskSearch.js +35 -0
  68. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  69. package/dist/task/ordinaryArchive.js +42 -0
  70. package/dist/tmux/tmuxManager.js +8 -8
  71. package/dist/web/assets/assetManifest.js +11 -51
  72. package/dist/web/assets/client/app/catalog.js +215 -0
  73. package/dist/web/assets/client/app/main.js +148 -0
  74. package/dist/web/assets/client/app/selection.js +139 -0
  75. package/dist/web/assets/client/app/settings.js +326 -0
  76. package/dist/web/assets/client/app/taskView.js +280 -0
  77. package/dist/web/assets/client/domain/context.js +96 -0
  78. package/dist/web/assets/client/domain/records.js +126 -0
  79. package/dist/web/assets/client/domain/runs.js +119 -0
  80. package/dist/web/assets/client/domain/taskForms.js +197 -0
  81. package/dist/web/assets/client/domain/vocab.js +69 -0
  82. package/dist/web/assets/client/domain/work.js +116 -0
  83. package/dist/web/assets/client/i18n/en.js +900 -0
  84. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  85. package/dist/web/assets/client/index.js +114 -0
  86. package/dist/web/assets/client/layout/resizer.js +37 -0
  87. package/dist/web/assets/client/layout/router.js +35 -0
  88. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  89. package/dist/web/assets/client/layout/sizing.js +95 -0
  90. package/dist/web/assets/client/layout/workspace.js +247 -0
  91. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  92. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  93. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  94. package/dist/web/assets/client/lib/i18n.js +54 -0
  95. package/dist/web/assets/client/lib/markdown.js +167 -0
  96. package/dist/web/assets/client/lib/prefs.js +28 -0
  97. package/dist/web/assets/client/lib/theme.js +68 -0
  98. package/dist/web/assets/client/ui/containers.js +101 -0
  99. package/dist/web/assets/client/ui/controls.js +51 -0
  100. package/dist/web/assets/client/ui/forms.js +48 -0
  101. package/dist/web/assets/client/ui/metrics.js +137 -0
  102. package/dist/web/assets/client/ui/primitives.js +92 -0
  103. package/dist/web/assets/client/ui/text.js +33 -0
  104. package/dist/web/assets/client/ui/toast.js +12 -0
  105. package/dist/web/assets/client/views/bodySearch.js +172 -0
  106. package/dist/web/assets/client/views/createTask.js +85 -0
  107. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  108. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  109. package/dist/web/assets/client/views/dock/preview.js +129 -0
  110. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  111. package/dist/web/assets/client/views/globalInput.js +67 -0
  112. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  113. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  114. package/dist/web/assets/client/views/skills.js +152 -0
  115. package/dist/web/assets/client/views/task/actions.js +36 -0
  116. package/dist/web/assets/client/views/task/delivery.js +185 -0
  117. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  118. package/dist/web/assets/client/views/task/files.js +272 -0
  119. package/dist/web/assets/client/views/task/observation.js +153 -0
  120. package/dist/web/assets/client/views/task/overview.js +161 -0
  121. package/dist/web/assets/client/views/task/page.js +131 -0
  122. package/dist/web/assets/client/views/task/records.js +163 -0
  123. package/dist/web/assets/client/views/task/runtime.js +130 -0
  124. package/dist/web/assets/client/views/task/timeline.js +190 -0
  125. package/dist/web/assets/shared/geometry.js +19 -0
  126. package/dist/web/assets/shared/taskTabs.js +8 -0
  127. package/dist/web/assets/shared/themes.js +93 -0
  128. package/dist/web/assets/shell/dialogs.js +63 -0
  129. package/dist/web/assets/shell/dock.js +40 -0
  130. package/dist/web/assets/shell/index.js +40 -0
  131. package/dist/web/assets/shell/settings.js +34 -0
  132. package/dist/web/assets/shell/sidebar.js +45 -0
  133. package/dist/web/assets/styles/index.js +41 -0
  134. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  135. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  136. package/dist/web/assets/styles/markdown.js +22 -3
  137. package/dist/web/assets/styles/tokens.js +10 -63
  138. package/dist/web/assets/styles/ui/controls.js +76 -0
  139. package/dist/web/assets/styles/ui/forms.js +46 -0
  140. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  141. package/dist/web/assets/styles/views/delivery.js +29 -0
  142. package/dist/web/assets/styles/views/dock.js +71 -0
  143. package/dist/web/assets/styles/views/page.js +32 -0
  144. package/dist/web/assets/styles/views/records.js +70 -0
  145. package/dist/web/assets/styles/views/settings.js +26 -0
  146. package/dist/web/assets/styles/views/sidebar.js +74 -0
  147. package/dist/web/assets/styles/views/task.js +80 -0
  148. package/dist/web/assets/styles/views/terminal.js +38 -0
  149. package/dist/web/taskPreviews.js +205 -0
  150. package/dist/web/tmuxWebTerminal.js +16 -9
  151. package/dist/web/webConversation.js +373 -0
  152. package/dist/web/webMaterials.js +155 -0
  153. package/dist/web/webServer.js +295 -18
  154. package/dist/web/webSettings.js +331 -0
  155. package/dist/web/webSkills.js +86 -0
  156. package/dist/web/webSnapshot.js +40 -8
  157. package/dist/web/webTaskSurface.js +106 -8
  158. package/dist/workspace/gitChangeSetCapture.js +3 -1
  159. package/docs/managed-turn-and-session-runtime.md +9 -0
  160. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  161. package/docs/release-workflow.md +28 -0
  162. package/docs/release-workflow.zh-CN.md +21 -0
  163. package/docs/storage-baseline.md +38 -1
  164. package/docs/storage-baseline.zh-CN.md +22 -0
  165. package/docs/task-discovery.md +28 -0
  166. package/docs/testing/verification-levels.md +2 -2
  167. package/i18n/README.zh-CN.md +1 -1
  168. package/package.json +1 -1
  169. package/dist/web/assets/client/app.js +0 -808
  170. package/dist/web/assets/client/components.js +0 -477
  171. package/dist/web/assets/client/detail.js +0 -379
  172. package/dist/web/assets/client/dock.js +0 -309
  173. package/dist/web/assets/client/evidence.js +0 -364
  174. package/dist/web/assets/client/forms.js +0 -230
  175. package/dist/web/assets/client/i18n.js +0 -1277
  176. package/dist/web/assets/client/markdown.js +0 -114
  177. package/dist/web/assets/client/records.js +0 -198
  178. package/dist/web/assets/client/sections.js +0 -349
  179. package/dist/web/assets/client/theme.js +0 -71
  180. package/dist/web/assets/shell.js +0 -161
  181. package/dist/web/assets/styles/components.js +0 -216
  182. package/dist/web/assets/styles/views.js +0 -309
  183. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -63,13 +63,17 @@ export function assertProjectActive(project, action) {
63
63
  throw new Error(`Project is retired and cannot ${action}: ${project.id}.`);
64
64
  }
65
65
  }
66
- export function addProjectKnowledge(project, id, title, body, now, provenance) {
66
+ export function addProjectKnowledge(project, id, title, body, now, provenance, applicability = {}) {
67
67
  const timestamp = now.toISOString();
68
68
  const knowledge = {
69
- schemaVersion: 1,
69
+ schemaVersion: 2,
70
+ version: 1,
71
+ history: [],
70
72
  id: requireIdentity(id, "Project knowledge id"),
71
73
  title: requireText(title, "Project knowledge title"),
72
74
  body: requireText(body, "Project knowledge body"),
75
+ ...(applicability.scope === undefined ? {} : { scope: requireText(applicability.scope, "Knowledge scope") }),
76
+ ...(applicability.expiresWhen === undefined ? {} : { expiresWhen: requireText(applicability.expiresWhen, "Knowledge expiry condition") }),
73
77
  status: "active",
74
78
  ...(provenance === undefined ? {} : { provenance: validateKnowledgeProvenance(provenance) }),
75
79
  createdAt: timestamp,
@@ -114,19 +118,34 @@ export function updateProjectKnowledge(project, id, patch, now) {
114
118
  if (entry.status === "retired") {
115
119
  throw new Error(`Project knowledge is retired: ${knowledgeId}.`);
116
120
  }
117
- return {
118
- ...entry,
121
+ const { scope: _scope, expiresWhen: _expiresWhen, ...base } = entry;
122
+ const scope = patch.scope === undefined ? entry.scope
123
+ : patch.scope === null ? undefined : requireText(patch.scope, "Knowledge scope");
124
+ const expiresWhen = patch.expiresWhen === undefined ? entry.expiresWhen
125
+ : patch.expiresWhen === null ? undefined : requireText(patch.expiresWhen, "Knowledge expiry condition");
126
+ const next = {
127
+ ...base,
128
+ ...(scope === undefined ? {} : { scope }),
129
+ ...(expiresWhen === undefined ? {} : { expiresWhen }),
119
130
  title: patch.title === undefined
120
131
  ? entry.title
121
132
  : requireText(patch.title, "Project knowledge title"),
122
133
  body: patch.body === undefined
123
134
  ? entry.body
124
135
  : requireText(patch.body, "Project knowledge body"),
136
+ ...(patch.provenance === undefined ? {} : { provenance: validateKnowledgeProvenance(patch.provenance) }),
125
137
  updatedAt: now.toISOString()
126
138
  };
139
+ if (next.title === entry.title && next.body === entry.body
140
+ && next.scope === entry.scope && next.expiresWhen === entry.expiresWhen
141
+ && JSON.stringify(next.provenance) === JSON.stringify(entry.provenance))
142
+ return entry;
143
+ return { ...next, version: entry.version + 1, history: [...entry.history, knowledgeRevision(entry)] };
127
144
  });
128
145
  if (!found)
129
146
  throw new Error(`Project knowledge not found: ${knowledgeId}.`);
147
+ if (knowledge.every((entry, index) => entry === project.knowledge[index]))
148
+ return project;
130
149
  return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
131
150
  }
132
151
  export function retireProjectKnowledge(project, id, now) {
@@ -136,31 +155,42 @@ export function retireProjectKnowledge(project, id, now) {
136
155
  if (entry.id !== knowledgeId)
137
156
  return entry;
138
157
  found = true;
158
+ if (entry.status === "retired")
159
+ return entry;
139
160
  return {
140
161
  ...entry,
162
+ version: entry.version + 1,
163
+ history: [...entry.history, knowledgeRevision(entry)],
141
164
  status: "retired",
142
165
  updatedAt: now.toISOString()
143
166
  };
144
167
  });
145
168
  if (!found)
146
169
  throw new Error(`Project knowledge not found: ${knowledgeId}.`);
170
+ if (knowledge.every((entry, index) => entry === project.knowledge[index]))
171
+ return project;
147
172
  return validateProject({ ...project, knowledge, updatedAt: now.toISOString() });
148
173
  }
174
+ function knowledgeRevision(entry) {
175
+ const { schemaVersion: _schema, id: _id, history: _history, createdAt: _created, ...revision } = entry;
176
+ return revision;
177
+ }
149
178
  /**
150
179
  * Stable dedup fingerprint for a promotion candidate. Two proposals with the
151
- * same source identity, title, and body are the same conclusion regardless of
152
- * who proposed them or when.
180
+ * same source, content, applicability and replacement intent are identical.
181
+ * Trim outer whitespace only; preserve case, internal whitespace and Unicode.
182
+ * A JSON tuple avoids collisions between user-supplied identity delimiters.
153
183
  */
154
184
  export function knowledgeProposalFingerprint(input) {
155
- const sourceKey = [
156
- input.projectId,
157
- input.source.taskId,
158
- input.source.decisionId ?? "",
159
- input.source.milestoneId ?? "",
160
- input.source.commitSha ?? ""
161
- ].join("|");
185
+ const optional = (value) => value === undefined ? null : requireText(value, "Knowledge fingerprint field");
162
186
  return createHash("sha256")
163
- .update(`${sourceKey}\u0000${input.title}\u0000${input.body}`)
187
+ .update(JSON.stringify([
188
+ "knowledge-proposal-v2", requireText(input.projectId, "Project id"),
189
+ requireText(input.source.taskId, "Source Task"), optional(input.source.decisionId),
190
+ optional(input.source.milestoneId), optional(input.source.commitSha),
191
+ requireText(input.title, "Knowledge title"), requireText(input.body, "Knowledge body"),
192
+ optional(input.scope), optional(input.expiresWhen), optional(input.supersedesKnowledgeId)
193
+ ]))
164
194
  .digest("hex");
165
195
  }
166
196
  /** Digest of the cited source evidence, so a later Agent can verify it. */
@@ -334,17 +364,18 @@ export function validateProject(project) {
334
364
  requireGitRef(project.developmentBranch, "Project development branch");
335
365
  const knowledgeIds = new Set();
336
366
  for (const entry of project.knowledge) {
337
- if (entry.schemaVersion !== 1)
338
- throw new Error("Project knowledge must use schemaVersion 1.");
367
+ if (entry.schemaVersion !== 2)
368
+ throw new Error("Project knowledge must use schemaVersion 2.");
339
369
  requireIdentity(entry.id, "Project knowledge id");
340
- requireText(entry.title, "Project knowledge title");
341
- requireText(entry.body, "Project knowledge body");
342
- if (!["active", "retired"].includes(entry.status)) {
343
- throw new Error("Project knowledge status is invalid.");
344
- }
345
- if (entry.provenance !== undefined) {
346
- validateKnowledgeProvenance(entry.provenance);
370
+ validateKnowledgeRevision(entry);
371
+ if (!Array.isArray(entry.history) || entry.history.length !== entry.version - 1) {
372
+ throw new Error("Project knowledge history must contain every prior version.");
347
373
  }
374
+ entry.history.forEach((revision, index) => {
375
+ validateKnowledgeRevision(revision);
376
+ if (revision.version !== index + 1)
377
+ throw new Error("Project knowledge history is out of order.");
378
+ });
348
379
  requireTimestamp(entry.createdAt, "Project knowledge createdAt");
349
380
  requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
350
381
  if (knowledgeIds.has(entry.id))
@@ -363,6 +394,21 @@ export function validateProject(project) {
363
394
  requireTimestamp(project.updatedAt, "Project updatedAt");
364
395
  return project;
365
396
  }
397
+ function validateKnowledgeRevision(entry) {
398
+ if (!Number.isSafeInteger(entry.version) || entry.version < 1)
399
+ throw new Error("Invalid knowledge version.");
400
+ requireText(entry.title, "Project knowledge title");
401
+ requireText(entry.body, "Project knowledge body");
402
+ if (entry.scope !== undefined)
403
+ requireText(entry.scope, "Knowledge scope");
404
+ if (entry.expiresWhen !== undefined)
405
+ requireText(entry.expiresWhen, "Knowledge expiry condition");
406
+ if (!["active", "retired"].includes(entry.status))
407
+ throw new Error("Project knowledge status is invalid.");
408
+ if (entry.provenance !== undefined)
409
+ validateKnowledgeProvenance(entry.provenance);
410
+ requireTimestamp(entry.updatedAt, "Project knowledge updatedAt");
411
+ }
366
412
  function validateKnowledgeProvenance(provenance) {
367
413
  requireIdentity(provenance.taskId, "Knowledge provenance task id");
368
414
  if (provenance.decisionId !== undefined) {
@@ -394,8 +440,8 @@ function validateKnowledgeProvenance(provenance) {
394
440
  return provenance;
395
441
  }
396
442
  function validateKnowledgeProposal(proposal, projectId) {
397
- if (proposal.schemaVersion !== 1) {
398
- throw new Error("Knowledge proposal must use schemaVersion 1.");
443
+ if (proposal.schemaVersion !== 2) {
444
+ throw new Error("Knowledge proposal must use schemaVersion 2.");
399
445
  }
400
446
  requireIdentity(proposal.id, "Knowledge proposal id");
401
447
  if (proposal.projectId !== projectId) {
@@ -403,6 +449,9 @@ function validateKnowledgeProposal(proposal, projectId) {
403
449
  }
404
450
  requireText(proposal.title, "Knowledge proposal title");
405
451
  requireText(proposal.body, "Knowledge proposal body");
452
+ if (proposal.fingerprint !== knowledgeProposalFingerprint(proposal)) {
453
+ throw new Error("Knowledge proposal fingerprint does not match its content.");
454
+ }
406
455
  if (!["pending", "accepted", "rejected"].includes(proposal.status)) {
407
456
  throw new Error("Knowledge proposal status is invalid.");
408
457
  }
@@ -10,6 +10,7 @@ import { execFile } from "node:child_process";
10
10
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
11
11
  import { basename, join, resolve } from "node:path";
12
12
  import { promisify } from "node:util";
13
+ import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
13
14
  import { managedProjectPath } from "../repository/project.js";
14
15
  import { managedRuntimeRoot } from "../storage/homeLayout.js";
15
16
  import { isResourceQuarantinePath } from "./resourceRegistry.js";
@@ -54,8 +55,7 @@ export function parseGitWorktreePorcelain(output) {
54
55
  }
55
56
  async function listGitWorktrees(repositoryPath) {
56
57
  try {
57
- const { stdout } = await executeFile("git", ["-C", repositoryPath, "worktree", "list", "--porcelain"], { timeout: 10_000 });
58
- return parseGitWorktreePorcelain(stdout);
58
+ return await readGitWorktrees(repositoryPath);
59
59
  }
60
60
  catch (error) {
61
61
  throw new Error(`Failed to list Git worktrees for ${repositoryPath}: `
@@ -16,6 +16,7 @@
16
16
  * - `restoreAllResourceGc` is the explicit rollback entry point.
17
17
  */
18
18
  import { execFileSync } from "node:child_process";
19
+ import { supportedGit } from "../repository/gitCompatibility.js";
19
20
  import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
20
21
  import { dirname, join, resolve, relative, isAbsolute } from "node:path";
21
22
  import { isDeepStrictEqual } from "node:util";
@@ -384,13 +385,14 @@ function quarantineResource(home, record, now) {
384
385
  const receiptPath = `${quarantinePath}.receipt.json`;
385
386
  try {
386
387
  if (isGitWorktreePath(record.path)) {
388
+ const program = supportedGit();
387
389
  // Move the linked worktree, including its exact checkout and Git
388
390
  // metadata, into the Home-local quarantine. Restore is the inverse
389
391
  // move, so the recorded HEAD cannot drift with a branch.
390
392
  mkdirSync(quarantineRoot, { recursive: true, mode: 0o700 });
391
393
  // Keep the existing SQLite writer fence through this bounded move, so a
392
394
  // Task reopen or new durable owner cannot commit between check and move.
393
- execFileSync("git", ["-C", record.path, "worktree", "move", "--", record.path, quarantinePath], { timeout: 30_000 });
395
+ execFileSync(program.path, ["-C", record.path, "worktree", "move", "--", record.path, quarantinePath], { timeout: 30_000 });
394
396
  writeQuarantineReceipt(home, record, now, "move", receiptPath);
395
397
  return {
396
398
  ok: true,
@@ -496,9 +498,10 @@ function restoreQuarantinedRecord(record, liveRefs, now) {
496
498
  updatedAt: now.toISOString()
497
499
  };
498
500
  }
501
+ const program = isGitWorktreePath(quarantine.path) ? supportedGit() : undefined;
499
502
  mkdirSync(dirname(quarantine.originalPath), { recursive: true });
500
- if (isGitWorktreePath(quarantine.path)) {
501
- execFileSync("git", ["-C", quarantine.path, "worktree", "move", "--", quarantine.path, quarantine.originalPath], { timeout: 30_000 });
503
+ if (program !== undefined) {
504
+ execFileSync(program.path, ["-C", quarantine.path, "worktree", "move", "--", quarantine.path, quarantine.originalPath], { timeout: 30_000 });
502
505
  }
503
506
  else {
504
507
  renameSync(quarantine.path, quarantine.originalPath);
@@ -604,7 +607,7 @@ export async function purgeResourceQuarantine(home, options, store) {
604
607
  try {
605
608
  if (existsSync(quarantine.path)) {
606
609
  if (isGitWorktreePath(quarantine.path)) {
607
- execFileSync("git", ["-C", quarantine.path, "worktree", "remove", "--force", "--", quarantine.path], { timeout: 30_000 });
610
+ execFileSync(supportedGit().path, ["-C", quarantine.path, "worktree", "remove", "--force", "--", quarantine.path], { timeout: 30_000 });
608
611
  }
609
612
  rmSync(quarantine.path, { recursive: true, force: true });
610
613
  }
@@ -9,6 +9,7 @@
9
9
  import { execFileSync } from "node:child_process";
10
10
  import { existsSync } from "node:fs";
11
11
  import { basename, dirname, resolve } from "node:path";
12
+ import { gitOutputLine, supportedGit } from "../repository/gitCompatibility.js";
12
13
  import { upsertResourceRecord } from "./resourceRegistry.js";
13
14
  import { createResourceRegistryStore } from "./resourceRegistryStore.js";
14
15
  import { createResourceRecord, isReleaseNamespacePath, resourceId } from "./resourceTypes.js";
@@ -212,7 +213,7 @@ function ownerFromManagedWorkspace(home, owner, projectId) {
212
213
  };
213
214
  }
214
215
  function readGitWorktreeMetadata(path) {
215
- const commonDir = execFileSync("git", ["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
216
+ const commonDir = resolve(path, gitOutputLine(execFileSync(supportedGit().path, ["-C", path, "rev-parse", "--git-common-dir"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })));
216
217
  const repositoryPath = basename(commonDir) === ".git" ? dirname(commonDir) : commonDir;
217
218
  let branch;
218
219
  try {
@@ -41,6 +41,12 @@ export function createAgentEndpointFactory(start = startStructuredProviderSessio
41
41
  endpoint.observe(event);
42
42
  };
43
43
  const opened = await start(payload, {
44
+ // The native TUI owns rendering while attached. Raw JSON mirroring into
45
+ // its inherited PTY would corrupt its screen on every Provider event.
46
+ mirrorOutput: (stream, text) => {
47
+ if (!endpoint?.nativeAccess?.attached)
48
+ (stream === "stdout" ? process.stdout : process.stderr).write(text);
49
+ },
44
50
  onAccepted: (value) => emit({ type: "accepted", value }),
45
51
  onActivity: (value) => emit({ type: "activity", value }),
46
52
  onStarted: (value) => emit({ type: "started", value }),
@@ -74,6 +80,7 @@ function freezeConfiguration(value) {
74
80
  class BuiltinAgentEndpoint {
75
81
  driver;
76
82
  configuration;
83
+ get nativeAccess() { return this.driver.nativeAccess; }
77
84
  get ownedProcessId() { return this.driver.ownedProcessId; }
78
85
  get nativeAccountHome() { return this.driver.nativeAccountHome; }
79
86
  capabilities;
@@ -118,6 +125,7 @@ class BuiltinAgentEndpoint {
118
125
  get nativeSessionId() { return this.driver.nativeSessionId; }
119
126
  get conversationId() { return this.driver.conversationId; }
120
127
  get processInstanceId() { return this.driver.processInstanceId; }
128
+ get publicReply() { return this.driver.publicReply; }
121
129
  /**
122
130
  * Delegated rather than copied at construction: the Session reads its own
123
131
  * current state, and a value captured here would freeze the configuration as
@@ -18,6 +18,7 @@ import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
18
18
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
19
19
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
20
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
21
+ import { openControlledCodexTui } from "./controlledCodexTui.js";
21
22
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
22
23
  import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
23
24
  import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
@@ -108,6 +109,10 @@ export async function runAgentHost(input) {
108
109
  let snapshot = hostSnapshot("idle");
109
110
  let dispatchTail = Promise.resolve();
110
111
  let promptHuman = () => { };
112
+ let humanConsole;
113
+ let nativeTui;
114
+ let nativeOpening;
115
+ let modelChanging = false;
111
116
  const recentSteerAttempts = new Map();
112
117
  await replayExitOutbox(input.home);
113
118
  const updateSnapshot = (next) => {
@@ -444,6 +449,8 @@ export async function runAgentHost(input) {
444
449
  && providerSession.adapterId === "codex"
445
450
  && !hostStopRequested;
446
451
  if (reconnectableCodexClient) {
452
+ nativeTui?.close();
453
+ nativeTui = undefined;
447
454
  session = undefined;
448
455
  if (codexClientAttachedAt !== undefined
449
456
  && Date.now() - codexClientAttachedAt >= CODEX_CLIENT_STABLE_MS) {
@@ -1028,7 +1035,128 @@ export async function runAgentHost(input) {
1028
1035
  promptHuman();
1029
1036
  return snapshot;
1030
1037
  };
1031
- const control = await openAgentHostControl(input.home, payload, () => snapshot, async (request) => {
1038
+ const nativeMutation = async (expected, method, params) => {
1039
+ if (session !== expected || params.threadId !== expected.nativeSessionId || !authority
1040
+ || (sessionPayload?.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")) {
1041
+ throw new Error("Native input targets a replaced or unsupported Session.");
1042
+ }
1043
+ if (method === "turn/interrupt") {
1044
+ if (!activeRunAttemptId || params.turnId !== activeNativeTurnId)
1045
+ throw new Error("Native stop targets a different Turn.");
1046
+ const cancelled = await expected.cancel(activeRunAttemptId);
1047
+ if (cancelled.status !== "requested")
1048
+ throw new Error(`Native stop is ${cancelled.status}; do not replay.`);
1049
+ return {};
1050
+ }
1051
+ if (!Array.isArray(params.input) || params.input.length === 0
1052
+ || params.input.some(part => !part || typeof part !== "object"
1053
+ || part.type !== "text" || typeof part.text !== "string")) {
1054
+ throw new Error("Managed native access currently supports text input only.");
1055
+ }
1056
+ const boundedText = params.input.map(part => part.text).join("\n");
1057
+ if (!boundedText.trim() || boundedText.includes("\0") || Buffer.byteLength(boundedText) > 12000)
1058
+ throw new Error("Native input exceeds its text bound.");
1059
+ const run = { attemptId: `human:${authority.holderId}:${randomUUID()}`, boundedText };
1060
+ if (method === "turn/steer") {
1061
+ const turnId = activeNativeTurnId;
1062
+ if (params.expectedTurnId !== turnId || !turnId)
1063
+ throw new Error("Native steer targets a different Turn.");
1064
+ if (!sessionPayload)
1065
+ throw new Error("Native Session launch context is unavailable.");
1066
+ // Persist human intent and the existing steer receipt before Provider
1067
+ // write. A fresh human turn id is not an additional-input binding.
1068
+ const prepared = await callControllerIdempotently(input.home, "runtime.native-steer-prepare", {
1069
+ ...hostRunControlParams(sessionPayload, expected.nativeSessionId, authority, run.attemptId),
1070
+ nativeTurnId: turnId, boundedText
1071
+ });
1072
+ const result = await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "steer-turn",
1073
+ nativeSessionId: expected.nativeSessionId, nativeTurnId: turnId, authority, run: prepared });
1074
+ if (result.outcome !== "accepted")
1075
+ throw new Error("Native steer acceptance is unconfirmed; do not resubmit.");
1076
+ return { turnId };
1077
+ }
1078
+ const result = await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn",
1079
+ nativeSessionId: expected.nativeSessionId, authority, run });
1080
+ const accepted = result.snapshot;
1081
+ if (result.outcome !== "accepted" || accepted.inputAcceptance !== "provider" || !accepted.nativeTurnId) {
1082
+ throw new Error("Native acceptance is unconfirmed; do not resubmit.");
1083
+ }
1084
+ return { turn: { id: accepted.nativeTurnId, items: [], status: "inProgress", error: null } };
1085
+ };
1086
+ const handleControl = async (request) => {
1087
+ if (request.type === "native-model") {
1088
+ return enqueueSerialized(async () => {
1089
+ const current = session;
1090
+ if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1091
+ || !sessionPayload || activeNativeTurnId || authority?.owner !== "controller"
1092
+ || (sessionPayload.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")
1093
+ || (sessionPayload.environment.YUI_SESSION_SCOPE === "global" && sessionPayload.environment.YUI_ROLE !== "operator")) {
1094
+ throw new Error("Model switching requires the idle current controlled Codex Session, without a native terminal writer.");
1095
+ }
1096
+ if (nativeTui || nativeOpening) {
1097
+ throw new Error("Exit the native TUI before changing the Session model. Closing a browser terminal viewer only detaches it; the Host-owned TUI remains open.");
1098
+ }
1099
+ modelChanging = true;
1100
+ try {
1101
+ await assertHostExecutionEnvironment(input.home, sessionPayload, current.nativeSessionId);
1102
+ try {
1103
+ await current.nativeAccess.setModel(request.model);
1104
+ }
1105
+ catch (error) {
1106
+ if (error instanceof CodexPreSubmissionError)
1107
+ throw error;
1108
+ throw new ProviderDeliveryUnknownError(`Native model adoption is unconfirmed: ${errorText(error)}`, `model:${current.nativeSessionId}`, { cause: error });
1109
+ }
1110
+ return controlResult("accepted", { ...snapshot, runConfiguration: current.runConfiguration });
1111
+ }
1112
+ finally {
1113
+ modelChanging = false;
1114
+ }
1115
+ });
1116
+ }
1117
+ if (request.type === "native-open" || request.type === "native-respond") {
1118
+ const current = session;
1119
+ if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1120
+ || !sessionPayload || (sessionPayload.environment.YUI_SESSION_SCOPE === "task"
1121
+ && sessionPayload.environment.YUI_ROLE !== "leader"))
1122
+ throw new Error("Native access requires the current controlled Codex Operator/Leader.");
1123
+ if (request.type === "native-respond") {
1124
+ await assertHostExecutionEnvironment(input.home, sessionPayload, current.nativeSessionId);
1125
+ await current.nativeAccess.respond(request.requestId, request.nativeSessionId, request.nativeTurnId ?? null, request.result);
1126
+ }
1127
+ else if (!nativeTui) {
1128
+ if (modelChanging)
1129
+ throw new Error("Wait for model adoption before attaching native input.");
1130
+ if (!process.stdin.isTTY)
1131
+ throw new Error("Native TUI requires the Host's owned terminal.");
1132
+ humanConsole?.close();
1133
+ humanConsole = undefined;
1134
+ nativeOpening ??= openControlledCodexTui(sessionPayload, current.nativeSessionId, current.nativeAccess, {
1135
+ mutate: (method, params) => nativeMutation(current, method, params),
1136
+ respond: async (requestId, nativeTurnId, result) => {
1137
+ await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-respond",
1138
+ nativeSessionId: current.nativeSessionId, requestId, nativeTurnId, result });
1139
+ },
1140
+ onExit: () => {
1141
+ nativeTui = undefined;
1142
+ process.stdout.write("\r\nNative TUI detached. Reattach the terminal to reopen this same Thread; the Agent Host remains running.\r\n");
1143
+ },
1144
+ onError: error => process.stderr.write(`Native TUI: ${errorText(error)}\n`)
1145
+ });
1146
+ try {
1147
+ nativeTui = await nativeOpening;
1148
+ }
1149
+ finally {
1150
+ nativeOpening = undefined;
1151
+ }
1152
+ if (session !== current) {
1153
+ nativeTui.close();
1154
+ nativeTui = undefined;
1155
+ throw new Error("Session changed while opening native access.");
1156
+ }
1157
+ }
1158
+ return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
1159
+ }
1032
1160
  if (request.type === "status") {
1033
1161
  // Read the Agent's configuration at answer time, not from the stored
1034
1162
  // snapshot. The Agent may have changed it since the last state
@@ -1036,10 +1164,20 @@ export async function runAgentHost(input) {
1036
1164
  // current reading rather than the one that was true at launch. Nothing is
1037
1165
  // sent to the Agent to obtain it: this reads what the Session has already
1038
1166
  // been told.
1167
+ let requestBytes = 0;
1168
+ const nativeRequests = session?.nativeAccess?.requests.filter(request => {
1169
+ requestBytes += Buffer.byteLength(JSON.stringify(request));
1170
+ return requestBytes <= 8000;
1171
+ }).slice(0, 8);
1039
1172
  return controlResult("status", session === undefined
1040
1173
  ? snapshot
1041
1174
  : validateSnapshot({
1042
1175
  ...snapshot,
1176
+ nativeTui: !!nativeTui,
1177
+ ...(nativeRequests === undefined ? {} : { nativeRequests,
1178
+ nativeRequestsOmitted: nativeRequests.length !== session.nativeAccess.requests.length }),
1179
+ ...(session.publicReply === undefined ? {} : { publicReply: session.publicReply }),
1180
+ ...(session.adapterId === "codex" ? { publicReplyObservation: "supported" } : {}),
1043
1181
  runConfiguration: session.runConfiguration
1044
1182
  }));
1045
1183
  }
@@ -1138,6 +1276,8 @@ export async function runAgentHost(input) {
1138
1276
  const accepted = await enqueueSerialized(async () => setAuthority(request));
1139
1277
  return controlResult("accepted", accepted);
1140
1278
  }
1279
+ if (request.type !== "launch")
1280
+ throw new Error("Unsupported Host request.");
1141
1281
  const redeemed = await redeem(input.home, request.ticket);
1142
1282
  if (activeRunPayload !== undefined
1143
1283
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
@@ -1149,8 +1289,9 @@ export async function runAgentHost(input) {
1149
1289
  }
1150
1290
  const accepted = await enqueueDispatch(redeemed);
1151
1291
  return controlResult("accepted", accepted);
1152
- });
1153
- const humanConsole = process.stdin.isTTY
1292
+ };
1293
+ const control = await openAgentHostControl(input.home, payload, () => snapshot, handleControl);
1294
+ humanConsole = process.stdin.isTTY
1154
1295
  ? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
1155
1296
  : undefined;
1156
1297
  promptHuman = () => {
@@ -1231,6 +1372,7 @@ export async function runAgentHost(input) {
1231
1372
  if (forceKillTimer !== undefined)
1232
1373
  clearTimeout(forceKillTimer);
1233
1374
  humanConsole?.close();
1375
+ nativeTui?.close();
1234
1376
  await closeAgentHostEndpoints({
1235
1377
  owner: endpointOwner, session, lease: endpointLease, adapterId: endpointAdapterId,
1236
1378
  attemptId: activeRunAttemptId ?? lastTerminal?.attemptId,
@@ -1309,6 +1451,9 @@ export async function sendAgentHostAuthorityControl(input) {
1309
1451
  export async function sendAgentHostCancelControl(input) {
1310
1452
  return await sendAgentHostControl(input);
1311
1453
  }
1454
+ export async function sendAgentHostNativeControl(input) {
1455
+ return sendAgentHostControl(input);
1456
+ }
1312
1457
  export async function inspectAgentHost(input) {
1313
1458
  const result = await sendAgentHostControl({
1314
1459
  ...input,
@@ -1550,6 +1695,12 @@ function boundControlResponse(result) {
1550
1695
  };
1551
1696
  if (withinControlBound(result))
1552
1697
  return result;
1698
+ if (result.snapshot.publicReply !== undefined) {
1699
+ const { publicReply: _omitted, ...snapshot } = result.snapshot;
1700
+ result = { ...result, snapshot };
1701
+ if (withinControlBound(result))
1702
+ return result;
1703
+ }
1553
1704
  const clip = (text, chars) => {
1554
1705
  if (text.length <= chars)
1555
1706
  return text;
@@ -1613,6 +1764,22 @@ function validateControl(control) {
1613
1764
  }
1614
1765
  if (control.type === "status")
1615
1766
  return Object.freeze({ ...control });
1767
+ if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
1768
+ validateIdentity(control.nativeSessionId, "native Session id");
1769
+ if (control.type === "native-model" && (typeof control.model !== "string"
1770
+ || !control.model.trim() || control.model.length > 200 || control.model.includes("\0"))) {
1771
+ throw new Error("Invalid native model.");
1772
+ }
1773
+ if (control.type === "native-respond") {
1774
+ if (control.nativeTurnId !== null)
1775
+ validateIdentity(control.nativeTurnId, "native Turn id");
1776
+ if ((typeof control.requestId !== "string" && typeof control.requestId !== "number")
1777
+ || control.result === null || typeof control.result !== "object" || Array.isArray(control.result)) {
1778
+ throw new Error("Invalid native request response.");
1779
+ }
1780
+ }
1781
+ return Object.freeze({ ...control });
1782
+ }
1616
1783
  if (control.type === "cancel") {
1617
1784
  if (control.nativeOnly !== undefined && typeof control.nativeOnly !== "boolean") {
1618
1785
  throw new Error("Native-only cancellation flag is invalid.");
@@ -42,7 +42,7 @@ export class CodexAppServerRuntime {
42
42
  cwd: text(input.cwd, "Codex thread cwd"),
43
43
  ...threadOptions(input)
44
44
  });
45
- return { conversationId: threadId(result) };
45
+ return { conversationId: threadId(result), raw: result };
46
46
  }
47
47
  async resumeConversation(conversationId, options = {}) {
48
48
  const id = text(conversationId, "Codex thread id");