@zq-silk/yui 2.1.0 → 2.2.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 (102) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commandCatalog.js +24 -3
  3. package/dist/cli/commandDiscovery.js +4 -1
  4. package/dist/cli.js +23 -3078
  5. package/dist/commands/taskCommands.js +79 -21
  6. package/dist/commands/taskIntegrationCommands.js +3 -1
  7. package/dist/commands/taskUpstreamCommands.js +3 -1
  8. package/dist/context/runContextPack.js +132 -5
  9. package/dist/context/sourceRunContext.js +4 -2
  10. package/dist/context/taskContext.js +6 -1
  11. package/dist/controlPlaneCli.js +3092 -0
  12. package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
  13. package/dist/controller/jobControl.js +21 -0
  14. package/dist/executor/agentExecutor.js +1 -1
  15. package/dist/executor/effectiveLaunch.js +18 -5
  16. package/dist/executor/fileRoleLaunchPlanner.js +15 -19
  17. package/dist/integration/gitIntegrationService.js +50 -15
  18. package/dist/message/messageContinuation.js +7 -2
  19. package/dist/nativeAgent/agent.js +176 -74
  20. package/dist/nativeAgent/cliDemo.js +37 -0
  21. package/dist/nativeAgent/codingTools.js +11 -0
  22. package/dist/nativeAgent/commandTool.js +215 -0
  23. package/dist/nativeAgent/compactionDemo.js +158 -0
  24. package/dist/nativeAgent/composition.js +71 -0
  25. package/dist/nativeAgent/context/budget.js +44 -0
  26. package/dist/nativeAgent/context/index.js +339 -0
  27. package/dist/nativeAgent/context/providerCompressor.js +103 -0
  28. package/dist/nativeAgent/demo.js +12 -1
  29. package/dist/nativeAgent/evaluation/cases.js +38 -0
  30. package/dist/nativeAgent/evaluation/checks.js +91 -0
  31. package/dist/nativeAgent/evaluation/demo.js +19 -0
  32. package/dist/nativeAgent/evaluation/files.js +54 -0
  33. package/dist/nativeAgent/evaluation/fixture.js +36 -0
  34. package/dist/nativeAgent/evaluation/index.js +239 -0
  35. package/dist/nativeAgent/executionOwner.js +209 -0
  36. package/dist/nativeAgent/filePatterns.js +170 -0
  37. package/dist/nativeAgent/fileToolsSupport.js +202 -0
  38. package/dist/nativeAgent/index.js +13 -0
  39. package/dist/nativeAgent/interaction/cli.js +358 -0
  40. package/dist/nativeAgent/interaction/contracts.js +1 -0
  41. package/dist/nativeAgent/interaction/index.js +3 -0
  42. package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
  43. package/dist/nativeAgent/interaction/renderer.js +34 -0
  44. package/dist/nativeAgent/localSafety.js +258 -0
  45. package/dist/nativeAgent/model/anthropicMessages.js +204 -0
  46. package/dist/nativeAgent/model/chatCompletions.js +210 -0
  47. package/dist/nativeAgent/model/errors.js +47 -0
  48. package/dist/nativeAgent/model/gateway.js +423 -0
  49. package/dist/nativeAgent/model/index.js +7 -0
  50. package/dist/nativeAgent/model/observationAdapter.js +21 -0
  51. package/dist/nativeAgent/model/protocols.js +19 -0
  52. package/dist/nativeAgent/model/responses.js +263 -0
  53. package/dist/nativeAgent/model/types.js +1 -0
  54. package/dist/nativeAgent/model/wire.js +73 -0
  55. package/dist/nativeAgent/observability/index.js +220 -0
  56. package/dist/nativeAgent/product/catalog.js +82 -0
  57. package/dist/nativeAgent/product/config.js +295 -0
  58. package/dist/nativeAgent/product/facts.js +30 -0
  59. package/dist/nativeAgent/product/index.js +62 -0
  60. package/dist/nativeAgent/product/location.js +44 -0
  61. package/dist/nativeAgent/product/runtime.js +276 -0
  62. package/dist/nativeAgent/product/storage.js +49 -0
  63. package/dist/nativeAgent/product/tools.js +47 -0
  64. package/dist/nativeAgent/product/transport.js +54 -0
  65. package/dist/nativeAgent/projectGuidance/index.js +425 -0
  66. package/dist/nativeAgent/searchTools.js +305 -0
  67. package/dist/nativeAgent/session/backends.js +293 -0
  68. package/dist/nativeAgent/session/catalog.js +97 -0
  69. package/dist/nativeAgent/session/catalogDemo.js +87 -0
  70. package/dist/nativeAgent/session/contracts.js +1 -0
  71. package/dist/nativeAgent/session/format.js +269 -0
  72. package/dist/nativeAgent/session/index.js +5 -0
  73. package/dist/nativeAgent/session/location.js +36 -0
  74. package/dist/nativeAgent/session/sqliteFormat.js +134 -0
  75. package/dist/nativeAgent/session/store.js +248 -0
  76. package/dist/nativeAgent/textTools.js +270 -133
  77. package/dist/nativeAgent/toolManager/executor.js +290 -0
  78. package/dist/nativeAgent/toolManager/index.js +4 -0
  79. package/dist/nativeAgent/validation.js +2 -2
  80. package/dist/task/taskAuthority.js +56 -0
  81. package/dist/web/assets/client/app.js +64 -3
  82. package/dist/web/assets/client/detail.js +6 -6
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/overview.js +12 -9
  85. package/dist/web/assets/client/sidebar.js +31 -2
  86. package/dist/web/assets/shell.js +2 -1
  87. package/dist/web/assets/styles/components.js +2 -0
  88. package/dist/web/assets/styles/layout.js +11 -4
  89. package/dist/web/assets/styles/responsive.js +12 -4
  90. package/dist/web/assets/styles/views.js +16 -13
  91. package/docs/agent-result-consumption.md +16 -0
  92. package/docs/agent-result-consumption.zh-CN.md +13 -0
  93. package/docs/examples/agent-offline.mjs +194 -0
  94. package/docs/native-agent.md +283 -0
  95. package/docs/release-workflow.md +47 -9
  96. package/docs/release-workflow.zh-CN.md +36 -6
  97. package/docs/roles-and-configuration.md +32 -0
  98. package/docs/roles-and-configuration.zh-CN.md +26 -0
  99. package/package.json +1 -1
  100. package/skills/yui-leader/SKILL.md +9 -0
  101. package/skills/yui-reviewer/SKILL.md +5 -0
  102. package/skills/yui-runtime/SKILL.md +7 -0
@@ -267,7 +267,8 @@ export class FileSchedulerStoreAdapter {
267
267
  default:
268
268
  outcome = "obsolete";
269
269
  }
270
- if (outcome === "applied" && !isRunTerminalObservation(input)) {
270
+ if (outcome === "applied" && !isRunTerminalObservation(input)
271
+ && input.kind !== "goal.updated" && input.kind !== "goal.cleared") {
271
272
  this.persistRuntimeObservation(input, now);
272
273
  }
273
274
  return outcome;
@@ -700,7 +701,7 @@ export class FileSchedulerStoreAdapter {
700
701
  return "applied";
701
702
  });
702
703
  }
703
- persistRuntimeObservation(input, now) {
704
+ persistRuntimeObservation(input, now, notifyGoal = true) {
704
705
  this.store.transaction((store) => {
705
706
  const taskId = input.fence.taskId;
706
707
  const events = store.listEvents(taskId);
@@ -762,8 +763,7 @@ export class FileSchedulerStoreAdapter {
762
763
  ? "provider-continuation-report"
763
764
  : "provider-continuation-settled", now);
764
765
  }
765
- if ((input.kind === "goal.cleared"
766
- || (input.kind === "goal.updated" && input.payload.goalStatus !== "active"))
766
+ if (notifyGoal && (input.kind === "goal.cleared" || input.kind === "goal.updated")
767
767
  && input.fence.roleName !== "leader") {
768
768
  routeRoleEvent(store, observationEvent, input.fence.roleName, input.kind === "goal.cleared"
769
769
  ? "provider-goal-cleared"
@@ -2363,6 +2363,8 @@ export class FileSchedulerStoreAdapter {
2363
2363
  observeProviderGoal(input, now) {
2364
2364
  return this.store.transaction((store) => {
2365
2365
  const taskId = input.fence.taskId;
2366
+ if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
2367
+ return "applied";
2366
2368
  const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
2367
2369
  const binding = sessions?.providerBinding;
2368
2370
  if (sessions === null || sessions === undefined || binding === null || binding === undefined
@@ -2383,7 +2385,16 @@ export class FileSchedulerStoreAdapter {
2383
2385
  ? {}
2384
2386
  : { tokenBudget: input.payload.goalTokenBudget })
2385
2387
  });
2388
+ // Startup/re-attachment observes current state, not necessarily a change.
2389
+ // Compare under the same transaction that persists and routes the fact.
2390
+ if (isDeepStrictEqual(binding.goal, updated.goal)) {
2391
+ // Retain the existing dedup evidence: an old empty observation replayed
2392
+ // after a later real update must not clear that newer state.
2393
+ this.persistRuntimeObservation(input, now, false);
2394
+ return "applied";
2395
+ }
2386
2396
  store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, now));
2397
+ this.persistRuntimeObservation(input, now);
2387
2398
  return "applied";
2388
2399
  });
2389
2400
  }
@@ -3127,14 +3138,16 @@ function mapRole(store, role) {
3127
3138
  ? resolveEffectiveLaunch({
3128
3139
  role,
3129
3140
  purpose,
3130
- ...(workspace === undefined ? {} : { workspace })
3141
+ ...(workspace === undefined ? {} : { workspace }),
3142
+ ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
3131
3143
  })
3132
3144
  : liveSession !== null && workspace?.owner.type === "task"
3133
3145
  ? effectiveLaunchWithTaskMainWorkspace(liveSession.effective, workspace)
3134
3146
  : liveSession?.effective ?? resolveEffectiveLaunch({
3135
3147
  role,
3136
3148
  purpose,
3137
- ...(workspace === undefined ? {} : { workspace })
3149
+ ...(workspace === undefined ? {} : { workspace }),
3150
+ ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
3138
3151
  });
3139
3152
  return {
3140
3153
  taskId: role.taskId,
@@ -19,6 +19,7 @@ import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/m
19
19
  import { CliError } from "../errors/cliError.js";
20
20
  import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
21
21
  import { assertContextRecordReadable } from "../context/taskContext.js";
22
+ import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
22
23
  export function createDurableJobControl(store) {
23
24
  const authority = createJobCallAuthority(store);
24
25
  return {
@@ -167,8 +168,27 @@ export function authorizeJobStart(store, job) {
167
168
  throw jobDomainError("Job caller binding was revoked; no execution was started.");
168
169
  }
169
170
  requireJobAssignment(store, job, { scope, role });
171
+ assertLeaderJobWriteScope(store, job, { scope, role });
170
172
  validateJobTarget(store, job);
171
173
  }
174
+ function assertLeaderJobWriteScope(store, target, caller) {
175
+ if (caller.scope !== "task" || caller.role !== "leader")
176
+ return;
177
+ if (target.owner.kind !== "task" && target.owner.kind !== "integration-attempt") {
178
+ throw jobControlError("UNAUTHORIZED", "Direct Leader delivery does not grant another Assignment or Review workspace. Use the owning executor for its Job.");
179
+ }
180
+ const { taskId, projectId } = target;
181
+ const session = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(taskId, caller.role));
182
+ try {
183
+ assertTaskProjectWriteAuthority(store, {
184
+ YUI_SESSION_SCOPE: "task", YUI_TASK_ID: taskId, YUI_ROLE: caller.role,
185
+ YUI_NATIVE_SESSION_ID: caller.nativeSessionId ?? session?.nativeSessionId
186
+ }, taskId, projectId);
187
+ }
188
+ catch (error) {
189
+ throw jobControlError("UNAUTHORIZED", error instanceof Error ? error.message : String(error));
190
+ }
191
+ }
172
192
  function requireJobAssignment(store, target, caller) {
173
193
  try {
174
194
  assertJobAssignmentScope(store, target, caller);
@@ -244,6 +264,7 @@ function assertNonSecretJobInput(params) {
244
264
  function validateStartParams(store, params) {
245
265
  validateJobTarget(store, params);
246
266
  assertCallerAuthorized(store, params.caller, params.taskId);
267
+ assertLeaderJobWriteScope(store, params, params.caller);
247
268
  if (params.caller.scope === "task") {
248
269
  const session = store.getTaskRoleSessionSet(params.taskId, params.caller.role);
249
270
  if (session?.sessions[session.activeAgentId]?.effective.executionAuthority !== "delivery") {
@@ -317,7 +317,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired) {
317
317
  if (roleSessionMayContinue(session.effective, desired))
318
318
  return "resume";
319
319
  throw new Error(`Role Agent session cannot continue under the next launch: ${agentId}. `
320
- + "Its Agent, adapter or physical workspace changed. Explicitly select a new Session "
320
+ + "Its Agent, adapter, physical workspace or captured authority changed. Explicitly select a new Session "
321
321
  + "after resolving existing execution and resource ownership.");
322
322
  }
323
323
  export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
@@ -117,8 +117,8 @@ export function sameEffectiveLaunch(existing, desired) {
117
117
  *
118
118
  * Only facts that make continuation impossible participate: the Session
119
119
  * protocol, the provider identity that owns the conversation, and the physical
120
- * workspace the Session runs in. Launch configuration such as model, effort,
121
- * permission, Role context, declared write scope, and AgentRun-scoped facts like
120
+ * workspace and captured authority the Session runs with. Launch configuration
121
+ * such as model, effort, provider permission, Role context, and AgentRun-scoped facts like
122
122
  * ReviewRound identity or candidate commits shape the next Host process
123
123
  * instead of ending the Session; that divergence is acknowledged where the
124
124
  * configuration changes and stays visible as launch provenance.
@@ -151,6 +151,8 @@ function sessionContinuitySnapshot(snapshot) {
151
151
  return {
152
152
  schemaVersion: snapshot.schemaVersion,
153
153
  executionAuthority: snapshot.executionAuthority,
154
+ profileAccess: snapshot.profileAccess,
155
+ writeProjectIds: snapshot.writeProjectIds,
154
156
  contextProtocolVersion: snapshot.contextProtocolVersion,
155
157
  agentId: snapshot.agentId,
156
158
  // Two products on one connection plan are two different conversations. A
@@ -329,6 +331,8 @@ function effectiveLaunchConfigUnchecked(snapshot) {
329
331
  : claudeConfigFromSnapshot(snapshot);
330
332
  }
331
333
  function effectiveWriteProjects(input, workspace) {
334
+ if (input.purpose === "planning" || input.executionAuthority === "planning")
335
+ return [];
332
336
  if (input.purpose === "review") {
333
337
  if (!("taskId" in input.role)) {
334
338
  throw new Error("Review launch requires a Task Role.");
@@ -358,9 +362,18 @@ function effectiveWriteProjects(input, workspace) {
358
362
  .map(({ projectId }) => projectId), "Workspace writable Project");
359
363
  if (!("taskId" in input.role))
360
364
  return [];
361
- // A managed workspace describes what exists, not who is authorized to
362
- // mutate it. Only an explicit WorkItem write scope can grant Task writes.
363
- if (input.workItemWriteProjectIds === undefined)
365
+ // Direct delivery belongs to the Leader in this Task's adopted main
366
+ // workspace. Assignment scope (including explicit []) still takes priority.
367
+ // Never infer authority from a path, a different owner's workspace or a
368
+ // desired Profile change to an already-frozen Session.
369
+ if (input.workItemWriteProjectIds === undefined) {
370
+ return input.role.name === "leader"
371
+ && input.role.defaultAccess === "write"
372
+ && input.workspace?.owner.type === "task"
373
+ && input.workspace.owner.taskId === input.role.taskId
374
+ ? workspaceWrite : [];
375
+ }
376
+ if (input.role.defaultAccess !== "write")
364
377
  return [];
365
378
  const requested = uniqueIdentities(input.workItemWriteProjectIds, "WorkItem writable Project");
366
379
  if (requested.length === 0)
@@ -216,7 +216,7 @@ export class FileRoleLaunchPlanner {
216
216
  // attempt was rejected, cancelled, or completed.
217
217
  assertProviderConversationReplaceable(sessionSet.providerBinding);
218
218
  }
219
- return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
219
+ return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
220
220
  purpose
221
221
  });
222
222
  }
@@ -246,9 +246,9 @@ export class FileRoleLaunchPlanner {
246
246
  }
247
247
  return this.#compile(role, input, { scope: "global" }, input.mode === "new" && role.name === "operator"
248
248
  ? operatorSessionTitle(this.#now(), this.store.getConfig().timeZone)
249
- : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
249
+ : undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
250
250
  }
251
- #compile(role, input, owner, sessionTitle, knownNativeSessionId, workspaceOverride, effective, sessionPolicy) {
251
+ #compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
252
252
  const launchRole = effectiveRoleForLaunch(role, effective);
253
253
  const binding = activeRoleAgentBinding(launchRole);
254
254
  if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
@@ -580,7 +580,7 @@ export class FileRoleLaunchPlanner {
580
580
  childLifecycle: driver.capabilities.lifecycle.providerProcess
581
581
  };
582
582
  const scopedLaunch = owner.scope === "task"
583
- ? this.#applyWorkspaceScope(owner.taskId, role, launch, workspaceOverride)
583
+ ? this.#applyWorkspaceScope(launch, effective)
584
584
  : launch;
585
585
  const ordinaryConversationLaunch = withCodexThreadEnvironment(scopedLaunch);
586
586
  return {
@@ -640,20 +640,11 @@ export class FileRoleLaunchPlanner {
640
640
  }
641
641
  return { attemptId: run.attemptId, turnId: run.nativeTurnId };
642
642
  }
643
- #applyWorkspaceScope(taskId, role, launch, workspaceOverride) {
644
- const workspace = workspaceOverride
645
- ?? (role.name === "leader"
646
- ? this.store.getTaskWorkspace(taskId)
647
- : this.store.listWorkItems(taskId)
648
- .find((item) => item.assignee === role.name && item.status === "open") === undefined
649
- ? this.store.getTaskWorkspace(taskId)
650
- : this.store.getWorkItemWorkspace(taskId, this.store.listWorkItems(taskId).find((item) => item.assignee === role.name && item.status === "open").id));
651
- if (workspace === null || workspace === undefined)
652
- return launch;
643
+ #applyWorkspaceScope(launch, effective) {
653
644
  return {
654
645
  ...launch,
655
646
  env: {
656
- ...workspaceScopeEnvironment(launch.env, workspace)
647
+ ...workspaceScopeEnvironment(launch.env, effective)
657
648
  }
658
649
  };
659
650
  }
@@ -711,16 +702,21 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
711
702
  ...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
712
703
  });
713
704
  }
714
- function workspaceScopeEnvironment(environment, workspace) {
705
+ function workspaceScopeEnvironment(environment, effective) {
706
+ const entries = effective.workspace.entries.map(entry => ({
707
+ ...entry,
708
+ access: entry.access === "write" && effective.writeProjectIds.includes(entry.projectId)
709
+ ? "write" : "read"
710
+ }));
715
711
  return {
716
712
  ...environment,
717
- YUI_WRITABLE_PROJECT_IDS: JSON.stringify(workspace.entries
713
+ YUI_WRITABLE_PROJECT_IDS: JSON.stringify(entries
718
714
  .filter(({ access }) => access === "write")
719
715
  .map(({ projectId }) => projectId)),
720
- YUI_CONTEXT_PROJECT_IDS: JSON.stringify(workspace.entries
716
+ YUI_CONTEXT_PROJECT_IDS: JSON.stringify(entries
721
717
  .filter(({ access }) => access === "read")
722
718
  .map(({ projectId }) => projectId)),
723
- YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(workspace.entries.map(({ projectId, directory, access, path }) => [
719
+ YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(entries.map(({ projectId, directory, access, path }) => [
724
720
  projectId,
725
721
  { directory, access, path }
726
722
  ])))
@@ -1,5 +1,5 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
- import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
2
+ import { lstat, mkdir, open, realpath, rm, stat } from "node:fs/promises";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
  import { selectEnvironment } from "../agent/launchEnvironment.js";
@@ -14,6 +14,7 @@ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
14
14
  import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
15
15
  import { integrationTmuxSocketRoot, managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
16
16
  import { advanceTaskProjectCommit } from "../task/task.js";
17
+ import { taskIntegrationSettlementCheckout, taskIntegrationTargetCheckout } from "../task/taskAuthority.js";
17
18
  import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
18
19
  import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
19
20
  import { touchGateArtifact } from "../verification/gateArtifactStore.js";
@@ -75,8 +76,12 @@ export class GitIntegrationService {
75
76
  #resourceRegistrar() {
76
77
  return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
77
78
  }
79
+ #targetCheckout(attempt) {
80
+ return taskIntegrationTargetCheckout(this.store, this.environment, attempt.taskId, attempt.projectId, attempt.targetRef);
81
+ }
78
82
  async integrate(taskId, integrationId, signal) {
79
83
  let initial = requireIntegration(this.store, taskId, integrationId);
84
+ this.#targetCheckout(initial);
80
85
  if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
81
86
  throw new Error(`Integration cannot continue from ${initial.status}.`);
82
87
  }
@@ -95,6 +100,7 @@ export class GitIntegrationService {
95
100
  throw new Error("Integration changed before execution admission; read its current record.");
96
101
  }
97
102
  initial = currentUnderFence;
103
+ this.#targetCheckout(initial);
98
104
  const task = this.store.getTask(initial.taskId);
99
105
  if (task === null || !task.projectBindings.some(({ projectId }) => projectId === initial.projectId)) {
100
106
  throw new Error(`Integration Task Project is unavailable: ${initial.taskId}.`);
@@ -116,6 +122,7 @@ export class GitIntegrationService {
116
122
  || taskRepository === undefined) {
117
123
  throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
118
124
  }
125
+ await assertTargetCheckout(taskRepository, initial.targetRef, this.#targetCheckout(initial));
119
126
  // Issue 08: a Project with a VerificationPlan gates through its plan
120
127
  // (bootstrap + L2) and reuses exact-SHA artifacts; an unconfigured
121
128
  // Project keeps the existing explicit check path unchanged.
@@ -193,7 +200,7 @@ export class GitIntegrationService {
193
200
  // moved or its worktree is dirty, so the check commands never run on a
194
201
  // target that cannot be advanced. advanceTargetRef re-verifies both
195
202
  // after the checks, so a move during the gate is still fenced at CAS.
196
- await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit);
203
+ await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit, this.#targetCheckout(current));
197
204
  if (gate !== undefined) {
198
205
  return await this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
199
206
  }
@@ -221,7 +228,7 @@ export class GitIntegrationService {
221
228
  checks: checkResults
222
229
  }, this.now());
223
230
  this.store.saveIntegrationAttempt(task.id, current);
224
- await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit);
231
+ await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit, this.#targetCheckout(current));
225
232
  const committed = this.#recordCommitted(current);
226
233
  return this.#terminalResult("committed", committed, workspace);
227
234
  }
@@ -234,7 +241,7 @@ export class GitIntegrationService {
234
241
  if (target === current.candidateCommit) {
235
242
  try {
236
243
  await assertIntegrationCandidate(workspace.path, current.candidateCommit, workspace.branch);
237
- await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit);
244
+ await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit, this.#targetCheckout(current));
238
245
  const committed = this.#recordCommitted(current);
239
246
  return this.#terminalResult("committed", committed, workspace);
240
247
  }
@@ -307,8 +314,21 @@ export class GitIntegrationService {
307
314
  }
308
315
  const target = await resolveRef(mainEntry.path, attempt.targetRef);
309
316
  if (target === attempt.candidateCommit && target !== attempt.beforeCommit) {
310
- await assertTargetReadyForChecks(mainEntry.path, attempt.targetRef, target);
311
- return this.#recordCommitted(attempt, authorize);
317
+ if (attempt.checks.some(check => check.outcome === "failed")
318
+ || attempt.jobId !== undefined
319
+ && jobs.find(job => job.id === attempt.jobId).status !== "succeeded") {
320
+ throw new Error("Delivered Integration lacks successful candidate check evidence; inspect before settlement.");
321
+ }
322
+ await assertIntegrationCandidate(workspace.root, attempt.candidateCommit, workspaceEntry.branch);
323
+ assertRecordedSourceCandidate(attempt, { path: workspace.root, branch: workspaceEntry.branch });
324
+ await assertTargetReadyForChecks(mainEntry.path, attempt.targetRef, target, taskIntegrationSettlementCheckout(this.store, this.environment, taskId, attempt.projectId, attempt.targetRef));
325
+ return this.#recordCommitted(attempt, (tx, id) => {
326
+ authorize(tx, id);
327
+ taskIntegrationSettlementCheckout(tx, this.environment, id, attempt.projectId, attempt.targetRef);
328
+ if (JSON.stringify(requireIntegration(tx, id, integrationId)) !== JSON.stringify(attempt)) {
329
+ throw new Error("Integration changed before delivered settlement; inspect its current record.");
330
+ }
331
+ });
312
332
  }
313
333
  if (target !== attempt.beforeCommit) {
314
334
  throw new Error(`Target moved to ${target}; cannot prove this Integration is unadvanced. Inspect before settlement.`);
@@ -419,7 +439,7 @@ export class GitIntegrationService {
419
439
  throw new Error(`Integration check admission requires a running, proven candidate: ${attempt.id}.`);
420
440
  }
421
441
  await assertIntegrationCandidate(path, attempt.candidateCommit, workspace.branch);
422
- await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
442
+ await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
423
443
  const { runtime, head, steps, environment, inputDigest } = await this.#checkSpecification(attempt, managedWorkspace, gate);
424
444
  if (attempt.checkInputDigest !== undefined && attempt.checkInputDigest !== inputDigest) {
425
445
  throw new Error("Integration check conditions changed since admission; inspect the original Job, then abort or start a new attempt.");
@@ -479,7 +499,7 @@ export class GitIntegrationService {
479
499
  const job = await this.jobPort.getJob(attempt.taskId, attempt.jobId);
480
500
  assertCheckJobIdentity(attempt, job, path);
481
501
  await assertIntegrationCandidate(path, job.head, workspace.branch);
482
- await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
502
+ await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
483
503
  if (gate?.planDigest !== attempt.gatePlanDigest
484
504
  || gate?.toolchainDigest !== attempt.gateToolchainDigest) {
485
505
  throw new Error("Integration verification plan/toolchain changed; the original Job cannot prove the current gate.");
@@ -642,7 +662,7 @@ export class GitIntegrationService {
642
662
  checks
643
663
  }, this.now());
644
664
  this.store.saveIntegrationAttempt(attempt.taskId, validating);
645
- await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit);
665
+ await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit, this.#targetCheckout(validating));
646
666
  const committed = this.#recordCommitted(validating);
647
667
  return this.#terminalResult("committed", committed, workspace);
648
668
  }
@@ -709,9 +729,9 @@ export class GitIntegrationService {
709
729
  }
710
730
  assertCheckJobIdentity(attempt, job, workspace.path);
711
731
  }
712
- await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit);
732
+ await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit, this.#targetCheckout(attempt));
713
733
  }
714
- await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit);
734
+ await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit, this.#targetCheckout(attempt));
715
735
  const committed = this.#recordCommitted(attempt);
716
736
  return this.#terminalResult("committed", committed, workspace);
717
737
  }
@@ -1138,12 +1158,12 @@ async function resolveRef(repositoryPath, ref) {
1138
1158
  * {@link advanceTargetRef} re-verifies both before the CAS, so a target that
1139
1159
  * moves during the gate is still fenced.
1140
1160
  */
1141
- async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHead) {
1161
+ async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHead, expectedCheckout) {
1142
1162
  const current = await resolveRef(repositoryPath, targetRef);
1143
1163
  if (current !== expectedHead) {
1144
1164
  throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
1145
1165
  }
1146
- const checkedOutPaths = await checkedOutWorktreePaths(repositoryPath, fullTargetRef(targetRef));
1166
+ const checkedOutPaths = await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
1147
1167
  if (checkedOutPaths.length > 1) {
1148
1168
  throw new Error(`Integration target is checked out in multiple worktrees: ${targetRef}.`);
1149
1169
  }
@@ -1156,9 +1176,18 @@ async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHea
1156
1176
  }
1157
1177
  }
1158
1178
  }
1159
- async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expectedHead) {
1179
+ async function assertTargetCheckout(repositoryPath, targetRef, expectedCheckout) {
1180
+ const paths = await checkedOutWorktreePaths(repositoryPath, fullTargetRef(targetRef));
1181
+ if (expectedCheckout !== undefined && (await realpath(repositoryPath) !== await realpath(expectedCheckout)
1182
+ || paths.length !== 1 || await realpath(paths[0]) !== await realpath(expectedCheckout)
1183
+ || (await git(["-C", expectedCheckout, "symbolic-ref", "HEAD"])).trim() !== fullTargetRef(targetRef))) {
1184
+ throw new Error("Managed Leader Integration target must be physically checked out in its captured Task-main workspace.");
1185
+ }
1186
+ return paths;
1187
+ }
1188
+ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expectedHead, expectedCheckout) {
1160
1189
  const ref = fullTargetRef(targetRef);
1161
- const checkedOutPaths = await checkedOutWorktreePaths(repositoryPath, ref);
1190
+ const checkedOutPaths = await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
1162
1191
  if (checkedOutPaths.length === 0) {
1163
1192
  await git([
1164
1193
  "-C", repositoryPath, "update-ref",
@@ -1182,6 +1211,9 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
1182
1211
  if (current !== expectedHead) {
1183
1212
  throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
1184
1213
  }
1214
+ if (expectedCheckout !== undefined) {
1215
+ await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
1216
+ }
1185
1217
  // The candidate may be a rebased upstream result and therefore need not be
1186
1218
  // a descendant of the old Task head. Advance the branch with a real
1187
1219
  // compare-and-swap, then update only this checkout's index and worktree.
@@ -1193,6 +1225,9 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
1193
1225
  candidateCommit,
1194
1226
  expectedHead
1195
1227
  ]);
1228
+ if (expectedCheckout !== undefined) {
1229
+ await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
1230
+ }
1196
1231
  await git(["-C", checkout, "read-tree", "--reset", "-u", candidateCommit]);
1197
1232
  const advanced = await resolveRef(repositoryPath, targetRef);
1198
1233
  if (advanced !== candidateCommit) {
@@ -275,9 +275,14 @@ export function prepareMessageContinuations(store, taskId, now, roleName) {
275
275
  const { endedAt: _endedAt, failure: _failure, ...current } = round;
276
276
  return { ...current, status: "running", reviewerRunId: runId };
277
277
  })();
278
- const priorMessages = baseline.resources.filter((entry) => entry.ref.store === "task-message").slice(-16);
278
+ const messages = baseline.resources.filter((entry) => entry.ref.store === "task-message");
279
+ const priorMessages = [
280
+ ...messages.filter(entry => entry.ref.evidenceOf === "dispatch-required"),
281
+ ...messages.filter(entry => entry.ref.evidenceOf !== "dispatch-required").slice(-16)
282
+ ];
279
283
  const source = sourceRunContextValue(previous);
280
- const resources = [...baseline.resources.filter((entry) => entry.ref.store !== "task-message" && entry.ref.store !== "source-run"
284
+ const resources = [...baseline.resources.filter((entry) => entry.ref.store !== "task-message"
285
+ && (entry.ref.store !== "source-run" || entry.ref.evidenceOf !== undefined)
281
286
  && !(continuingRound !== null && entry.ref.store === "review-round" && entry.ref.refId === continuingRound.id)),
282
287
  ...(continuingRound === null ? [] : [{ ref: { layer: "L3", store: "review-round",
283
288
  refId: continuingRound.id, revision: now.toISOString(), digest: contextContentDigest(continuingRound),