@zq-silk/yui 0.6.0 → 0.6.2

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 (150) hide show
  1. package/README.md +5 -5
  2. package/dist/agent/managedRuntimeEnvironment.js +2 -1
  3. package/dist/cli/commandCatalog.js +251 -13
  4. package/dist/cli/updateOrchestrator.js +8 -0
  5. package/dist/cli/updatePorts.js +76 -22
  6. package/dist/cli.js +264 -20
  7. package/dist/commands/configCommands.js +83 -9
  8. package/dist/commands/controllerCommands.js +103 -0
  9. package/dist/commands/deliveryGuardPreflight.js +30 -0
  10. package/dist/commands/durableJobCommands.js +231 -0
  11. package/dist/commands/executionAuditCommands.js +193 -0
  12. package/dist/commands/grantCommands.js +374 -0
  13. package/dist/commands/projectCommands.js +119 -81
  14. package/dist/commands/releaseCommands.js +444 -0
  15. package/dist/commands/resourcesCommands.js +274 -0
  16. package/dist/commands/sessionCommands.js +104 -0
  17. package/dist/commands/taskActor.js +117 -0
  18. package/dist/commands/taskChangeSetCommands.js +60 -0
  19. package/dist/commands/taskCommands.js +618 -202
  20. package/dist/commands/taskCompletionGate.js +78 -1
  21. package/dist/commands/taskContextCommand.js +33 -6
  22. package/dist/commands/taskInputCommands.js +1 -1
  23. package/dist/commands/taskIntegrationCommands.js +136 -33
  24. package/dist/commands/taskIntegrationQueueCommands.js +228 -0
  25. package/dist/commands/taskNextActionCommand.js +100 -0
  26. package/dist/commands/taskOverlapCommands.js +120 -0
  27. package/dist/commands/taskOverviewCommand.js +36 -8
  28. package/dist/commands/telemetryCommands.js +330 -0
  29. package/dist/commands/workflowCommands.js +415 -0
  30. package/dist/config/yuiConfig.js +62 -0
  31. package/dist/controller/clientRuntime.js +42 -1
  32. package/dist/controller/controller.js +402 -56
  33. package/dist/controller/controllerMain.js +25 -2
  34. package/dist/controller/domainIdentity.js +16 -8
  35. package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
  36. package/dist/controller/handoverCandidate.js +168 -0
  37. package/dist/controller/jobClient.js +102 -0
  38. package/dist/controller/jobControl.js +613 -0
  39. package/dist/controller/jobSupervisor.js +498 -0
  40. package/dist/controller/providerHookRunFence.js +34 -5
  41. package/dist/controller/resourceCleanupLinux.js +18 -9
  42. package/dist/controller/resourceInventoryLinux.js +90 -39
  43. package/dist/controller/runtime.js +165 -15
  44. package/dist/controller/runtimeEventInbox.js +234 -57
  45. package/dist/controller/runtimeEventProcessor.js +297 -58
  46. package/dist/controller/sessionOwnerReconciliation.js +321 -0
  47. package/dist/core/controllerServer.js +416 -27
  48. package/dist/core/controllerTelemetry.js +167 -0
  49. package/dist/doctor/doctor.js +113 -16
  50. package/dist/domain/validation.js +9 -0
  51. package/dist/execution/executionGroup.js +40 -3
  52. package/dist/executor/agentExecutor.js +6 -3
  53. package/dist/executor/effectiveLaunch.js +52 -0
  54. package/dist/executor/executorRegistry.js +50 -0
  55. package/dist/executor/fileRoleLaunchPlanner.js +61 -6
  56. package/dist/grant/capabilityGrant.js +282 -0
  57. package/dist/integration/changeSet.js +16 -3
  58. package/dist/integration/changeSetManifest.js +46 -0
  59. package/dist/integration/gitIntegrationService.js +528 -147
  60. package/dist/integration/integrationAttempt.js +54 -5
  61. package/dist/integration/integrationQueueEntry.js +221 -0
  62. package/dist/integration/integrationQueueService.js +955 -0
  63. package/dist/integration/manifestTags.js +99 -0
  64. package/dist/integration/overlapDiagnostics.js +211 -0
  65. package/dist/job/durableJob.js +449 -0
  66. package/dist/job/jobRunner.js +350 -0
  67. package/dist/lifecycle/exactRunTerminalization.js +24 -2
  68. package/dist/lifecycle/providerErrorClass.js +126 -0
  69. package/dist/message/message.js +16 -3
  70. package/dist/observability/executionAudit.js +545 -0
  71. package/dist/observability/faultClassification.js +160 -0
  72. package/dist/observability/runtimeIdentity.js +367 -0
  73. package/dist/release/fakeReleasePorts.js +55 -0
  74. package/dist/release/releaseHandover.js +475 -0
  75. package/dist/release/releaseIdempotencyStore.js +165 -0
  76. package/dist/release/releaseWorkflow.js +459 -0
  77. package/dist/release/releaseWorkflowEngine.js +688 -0
  78. package/dist/release/releaseWorkflowPorts.js +1720 -0
  79. package/dist/release/runtimeRelease.js +495 -0
  80. package/dist/release/workflowFileLock.js +218 -0
  81. package/dist/repository/gitWorkspace.js +177 -1
  82. package/dist/repository/projectMaintenanceLock.js +315 -0
  83. package/dist/repository/taskWorkspaceCoordinator.js +87 -17
  84. package/dist/repository/taskWorkspacePreparer.js +1091 -517
  85. package/dist/resources/autoResourceGc.js +116 -0
  86. package/dist/resources/liveReferences.js +574 -0
  87. package/dist/resources/resourceDiscovery.js +477 -0
  88. package/dist/resources/resourceGc.js +645 -0
  89. package/dist/resources/resourceRegistrar.js +256 -0
  90. package/dist/resources/resourceRegistry.js +150 -0
  91. package/dist/resources/resourceRegistryStore.js +41 -0
  92. package/dist/resources/resourceTypes.js +42 -0
  93. package/dist/resources/sqliteResourceRegistry.js +111 -0
  94. package/dist/review/reviewConfig.js +10 -0
  95. package/dist/review/reviewFinding.js +240 -0
  96. package/dist/review/reviewFindingLedger.js +545 -0
  97. package/dist/review/reviewOutcomeClassifier.js +61 -0
  98. package/dist/review/reviewRound.js +56 -4
  99. package/dist/run/agentRun.js +80 -4
  100. package/dist/run/providerRetry.js +84 -0
  101. package/dist/run/providerRetryConfig.js +63 -0
  102. package/dist/run/yieldReceipt.js +65 -0
  103. package/dist/runtime/exactControlPlane.js +79 -2
  104. package/dist/runtime/index.js +4 -0
  105. package/dist/runtime/sessionOwnerIdentity.js +269 -0
  106. package/dist/runtime/sessionOwnerRegistry.js +132 -0
  107. package/dist/runtime/sessionReconciliation.js +93 -0
  108. package/dist/runtime/sessionTerminationGuard.js +211 -0
  109. package/dist/runtime/taskRuntimeIsolation.js +13 -0
  110. package/dist/runtime/tmuxAdapters.js +34 -1
  111. package/dist/scheduler/actionability.js +155 -0
  112. package/dist/scheduler/activeRoleRunDelivery.js +14 -5
  113. package/dist/scheduler/activeTaskProgress.js +60 -0
  114. package/dist/scheduler/leaderWakeupProcessor.js +22 -11
  115. package/dist/scheduler/roleRunStall.js +135 -29
  116. package/dist/scheduler/taskExecutionProjection.js +11 -0
  117. package/dist/storage/compatibleTaskStore.js +112 -5
  118. package/dist/storage/migration/productionRegistry.js +736 -1
  119. package/dist/storage/sqliteSchema.js +264 -3
  120. package/dist/storage/sqliteStore.js +487 -13
  121. package/dist/storage/storeRpc.js +21 -0
  122. package/dist/storage/taskStore.js +974 -21
  123. package/dist/storage/upgrade/homeClassification.js +120 -2
  124. package/dist/storage/upgrade/migrationReceipt.js +67 -0
  125. package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
  126. package/dist/storage/upgrade/recordVersions.js +10 -1
  127. package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
  128. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
  129. package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
  130. package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
  131. package/dist/task/deliveryGuard.js +226 -0
  132. package/dist/task/nextAction.js +738 -0
  133. package/dist/task/repairWave.js +137 -0
  134. package/dist/task/taskRecordReference.js +6 -1
  135. package/dist/telemetry/sqliteTelemetryStore.js +387 -0
  136. package/dist/telemetry/telemetryCompaction.js +251 -0
  137. package/dist/telemetry/telemetryConfig.js +64 -0
  138. package/dist/telemetry/telemetryRouter.js +32 -0
  139. package/dist/telemetry/telemetryStore.js +19 -0
  140. package/dist/telemetry/telemetryWiring.js +33 -0
  141. package/dist/tmux/tmuxManager.js +20 -1
  142. package/dist/tmux/tmuxSocketEndpoint.js +20 -0
  143. package/dist/verification/gateArtifact.js +216 -0
  144. package/dist/verification/gateArtifactStore.js +87 -0
  145. package/dist/verification/verificationGateService.js +414 -0
  146. package/dist/verification/verificationPlan.js +308 -0
  147. package/dist/workspace/gitChangeSetCapture.js +12 -2
  148. package/dist/workspace/workItemChangeSetManager.js +60 -3
  149. package/package.json +1 -1
  150. package/skills/yui-leader/SKILL.md +8 -0
@@ -0,0 +1,308 @@
1
+ import { createHash } from "node:crypto";
2
+ import { requireIdentity, requirePositiveInteger, requireText } from "../domain/validation.js";
3
+ /**
4
+ * Issue 08: the per-Project VerificationPlan contract.
5
+ *
6
+ * A Project's verification knowledge lives in its Yui-maintained Project
7
+ * record (a `verification-plan` knowledge entry, see
8
+ * {@link resolveProjectVerificationPlan}). The plan is Project Policy: it is
9
+ * never a hardcoded Yui default. A Project without a plan keeps the existing
10
+ * explicit `--check <shell>` path, which is marked unstructured and is not
11
+ * reusable across stages.
12
+ */
13
+ export const VERIFICATION_PLAN_SCHEMA_VERSION = 1;
14
+ export const VERIFICATION_PLAN_KIND = "verification-plan";
15
+ /** The reserved knowledge marker that carries a Project's VerificationPlan. */
16
+ export const VERIFICATION_PLAN_KNOWLEDGE_KIND = VERIFICATION_PLAN_KIND;
17
+ export function normalizeVerificationPlan(raw) {
18
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
19
+ throw new Error("VerificationPlan must be an object.");
20
+ }
21
+ const record = raw;
22
+ if (record.kind !== VERIFICATION_PLAN_KIND) {
23
+ throw new Error(`VerificationPlan kind must be "${VERIFICATION_PLAN_KIND}".`);
24
+ }
25
+ const schemaVersion = record.schemaVersion ?? VERIFICATION_PLAN_SCHEMA_VERSION;
26
+ if (schemaVersion !== VERIFICATION_PLAN_SCHEMA_VERSION) {
27
+ throw new Error(`VerificationPlan schemaVersion must be ${VERIFICATION_PLAN_SCHEMA_VERSION}.`);
28
+ }
29
+ const mode = record.mode ?? "record";
30
+ if (mode !== "record" && mode !== "reuse" && mode !== "enforce") {
31
+ throw new Error(`VerificationPlan mode is invalid: ${String(mode)}.`);
32
+ }
33
+ const plan = {
34
+ schemaVersion: VERIFICATION_PLAN_SCHEMA_VERSION,
35
+ kind: VERIFICATION_PLAN_KIND,
36
+ id: requireIdentity(record.id, "VerificationPlan id"),
37
+ version: requireText(record.version, "VerificationPlan version"),
38
+ mode,
39
+ toolchain: normalizeToolchain(record.toolchain),
40
+ bootstrap: normalizeSteps(record.bootstrap, "bootstrap"),
41
+ l1: {
42
+ categories: normalizeL1Categories(record.l1)
43
+ },
44
+ l2: {
45
+ steps: normalizeSteps(record.l2?.steps, "l2")
46
+ },
47
+ ...(record.l3 === undefined
48
+ ? {}
49
+ : {
50
+ l3: {
51
+ steps: normalizeSteps(record.l3?.steps, "l3")
52
+ }
53
+ }),
54
+ ...(record.excludedRealResourceChecks === undefined
55
+ ? {}
56
+ : {
57
+ excludedRealResourceChecks: normalizedTextList(record.excludedRealResourceChecks, "VerificationPlan excludedRealResourceChecks")
58
+ }),
59
+ ...(record.artifactTtlDays === undefined
60
+ ? {}
61
+ : {
62
+ artifactTtlDays: requirePositiveInteger(Number(record.artifactTtlDays), "VerificationPlan artifactTtlDays")
63
+ })
64
+ };
65
+ if (plan.l2.steps.length === 0) {
66
+ throw new Error("VerificationPlan l2 requires at least one step.");
67
+ }
68
+ return plan;
69
+ }
70
+ function normalizeToolchain(raw) {
71
+ if (raw === undefined)
72
+ return Object.freeze({});
73
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
74
+ throw new Error("VerificationPlan toolchain must be an object.");
75
+ }
76
+ const record = raw;
77
+ const toolchain = {
78
+ ...(record.node === undefined ? {} : { node: requireText(record.node, "VerificationPlan toolchain node") }),
79
+ ...(record.npm === undefined ? {} : { npm: requireText(record.npm, "VerificationPlan toolchain npm") }),
80
+ ...(record.platform === undefined ? {} : { platform: requireText(record.platform, "VerificationPlan toolchain platform") })
81
+ };
82
+ return toolchain;
83
+ }
84
+ function normalizeSteps(raw, label) {
85
+ if (!Array.isArray(raw)) {
86
+ throw new Error(`VerificationPlan ${label} steps must be an array.`);
87
+ }
88
+ const names = new Set();
89
+ return Object.freeze(raw.map((entry) => {
90
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
91
+ throw new Error(`VerificationPlan ${label} step must be an object.`);
92
+ }
93
+ const record = entry;
94
+ const name = requireIdentity(record.name, `VerificationPlan ${label} step name`);
95
+ if (names.has(name)) {
96
+ throw new Error(`VerificationPlan ${label} step names must be unique: ${name}.`);
97
+ }
98
+ names.add(name);
99
+ if (!Array.isArray(record.argv) || record.argv.length === 0) {
100
+ throw new Error(`VerificationPlan ${label} step ${name} requires a non-empty argv.`);
101
+ }
102
+ const argv = Object.freeze(record.argv.map((value) => requireText(value, `VerificationPlan ${label} step ${name} argv`)));
103
+ const step = {
104
+ name,
105
+ argv,
106
+ ...(record.cwd === undefined ? {} : { cwd: requireText(record.cwd, `VerificationPlan ${label} step ${name} cwd`) }),
107
+ ...(record.env === undefined ? {} : { env: normalizeStepEnv(record.env, `${label} step ${name}`) }),
108
+ ...(record.shell === undefined ? {} : { shell: record.shell === true })
109
+ };
110
+ if (step.cwd !== undefined && step.cwd.startsWith("/")) {
111
+ throw new Error(`VerificationPlan ${label} step ${name} cwd must be workspace-relative.`);
112
+ }
113
+ return step;
114
+ }));
115
+ }
116
+ function normalizeStepEnv(raw, label) {
117
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
118
+ throw new Error(`VerificationPlan ${label} env must be a map.`);
119
+ }
120
+ const env = {};
121
+ for (const [key, value] of Object.entries(raw)) {
122
+ if (typeof value !== "string") {
123
+ throw new Error(`VerificationPlan ${label} env values must be strings: ${key}.`);
124
+ }
125
+ env[requireIdentity(key, `VerificationPlan ${label} env key`)] = value;
126
+ }
127
+ return Object.freeze(env);
128
+ }
129
+ function normalizeL1Categories(raw) {
130
+ if (raw === undefined)
131
+ return Object.freeze([]);
132
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
133
+ throw new Error("VerificationPlan l1 must be an object.");
134
+ }
135
+ const categoriesRaw = raw.categories;
136
+ if (!Array.isArray(categoriesRaw)) {
137
+ throw new Error("VerificationPlan l1 categories must be an array.");
138
+ }
139
+ const ids = new Set();
140
+ return Object.freeze(categoriesRaw.map((entry) => {
141
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
142
+ throw new Error("VerificationPlan l1 category must be an object.");
143
+ }
144
+ const record = entry;
145
+ const id = requireIdentity(record.id, "VerificationPlan l1 category id");
146
+ if (ids.has(id)) {
147
+ throw new Error(`VerificationPlan l1 category ids must be unique: ${id}.`);
148
+ }
149
+ ids.add(id);
150
+ return Object.freeze({
151
+ id,
152
+ paths: normalizedTextList(record.paths, `VerificationPlan l1 category ${id} paths`),
153
+ checks: normalizeSteps(record.checks, `l1 ${id}`)
154
+ });
155
+ }));
156
+ }
157
+ function normalizedTextList(raw, label) {
158
+ if (!Array.isArray(raw)) {
159
+ throw new Error(`${label} must be an array.`);
160
+ }
161
+ const seen = new Set();
162
+ return Object.freeze(raw.map((value) => {
163
+ const text = requireText(value, label);
164
+ if (seen.has(text))
165
+ throw new Error(`${label} must be unique: ${text}.`);
166
+ seen.add(text);
167
+ return text;
168
+ }));
169
+ }
170
+ /**
171
+ * The stable digest of the gate contract. It deliberately excludes the
172
+ * rollout `mode`, retention window, and documentation fields: changing the
173
+ * rollout switch must not invalidate proven gate evidence.
174
+ */
175
+ export function verificationPlanDigest(plan) {
176
+ const canonical = canonicalJson({
177
+ id: plan.id,
178
+ version: plan.version,
179
+ toolchain: plan.toolchain,
180
+ bootstrap: plan.bootstrap,
181
+ l1: plan.l1,
182
+ l2: plan.l2,
183
+ ...(plan.l3 === undefined ? {} : { l3: plan.l3 })
184
+ });
185
+ return createHash("sha256").update(canonical).digest("hex");
186
+ }
187
+ /** Resolve the runtime toolchain identity from the current process. */
188
+ export function resolveToolchain() {
189
+ return Object.freeze({
190
+ node: process.version,
191
+ ...(process.env.npm_version === undefined ? {} : { npm: process.env.npm_version }),
192
+ platform: process.platform,
193
+ arch: process.arch
194
+ });
195
+ }
196
+ /**
197
+ * Digest of the toolchain a gate actually ran under, bound to the plan's
198
+ * declared requirements. A Node upgrade, platform change, or plan toolchain
199
+ * edit changes the digest and invalidates reuse.
200
+ */
201
+ export function toolchainDigest(plan, toolchain) {
202
+ const canonical = canonicalJson({
203
+ constraints: plan.toolchain,
204
+ node: toolchain.node,
205
+ ...(toolchain.npm === undefined ? {} : { npm: toolchain.npm }),
206
+ platform: toolchain.platform,
207
+ arch: toolchain.arch
208
+ });
209
+ return createHash("sha256").update(canonical).digest("hex");
210
+ }
211
+ /**
212
+ * Select the L1 checks for a change: every category whose path prefixes match
213
+ * a changed path. A category with no path prefixes matches nothing (it must
214
+ * be selected explicitly by the caller).
215
+ */
216
+ export function selectL1Checks(plan, changedPaths) {
217
+ const selected = new Map();
218
+ for (const category of plan.l1.categories) {
219
+ if (category.paths.length === 0)
220
+ continue;
221
+ const matches = changedPaths.some((path) => category.paths.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)));
222
+ if (!matches)
223
+ continue;
224
+ for (const check of category.checks) {
225
+ if (!selected.has(check.name))
226
+ selected.set(check.name, check);
227
+ }
228
+ }
229
+ return Object.freeze([...selected.values()]);
230
+ }
231
+ /** Bootstrap steps as DurableJob steps, named `bootstrap-N`. */
232
+ export function planBootstrapJobSteps(plan) {
233
+ return plan.bootstrap.map((step, index) => toDurableJobStep(step, `bootstrap-${index + 1}`));
234
+ }
235
+ /** L2 gate steps as DurableJob steps, named `gate-N`. */
236
+ export function planL2JobSteps(plan) {
237
+ return plan.l2.steps.map((step, index) => toDurableJobStep(step, `gate-${index + 1}`));
238
+ }
239
+ /** L1 steps as DurableJob steps, named `l1-N`. */
240
+ export function planL1JobSteps(steps) {
241
+ return steps.map((step, index) => toDurableJobStep(step, `l1-${index + 1}`));
242
+ }
243
+ function toDurableJobStep(step, name) {
244
+ return {
245
+ name,
246
+ // The shell-equivalent command stays as the human-readable fallback and
247
+ // the coverage-matching string; argv is the executable form.
248
+ command: step.shell === true ? step.argv.join(" ") : shellQuote(step.argv),
249
+ argv: step.argv,
250
+ ...(step.cwd === undefined ? {} : { cwd: step.cwd }),
251
+ ...(step.env === undefined ? {} : { env: step.env })
252
+ };
253
+ }
254
+ /** The shell-equivalent of a structured step, for display and coverage. */
255
+ export function verificationStepCommand(step) {
256
+ return step.shell === true ? step.argv.join(" ") : shellQuote(step.argv);
257
+ }
258
+ function shellQuote(argv) {
259
+ return argv.map((value) => (/^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`)).join(" ");
260
+ }
261
+ /**
262
+ * Resolve a Project's VerificationPlan from its Yui-maintained knowledge.
263
+ * A knowledge entry whose JSON body declares `kind: verification-plan` is
264
+ * the plan; malformed plan bodies fail closed. Free-text knowledge entries
265
+ * are ignored. A Project without a plan returns `undefined` (unstructured
266
+ * explicit-check mode).
267
+ */
268
+ export function resolveProjectVerificationPlan(project) {
269
+ let found;
270
+ for (const entry of project.knowledge) {
271
+ if (entry.status !== "active")
272
+ continue;
273
+ let parsed;
274
+ try {
275
+ parsed = JSON.parse(entry.body);
276
+ }
277
+ catch {
278
+ continue;
279
+ }
280
+ if (typeof parsed !== "object" || parsed === null
281
+ || parsed.kind !== VERIFICATION_PLAN_KIND) {
282
+ continue;
283
+ }
284
+ const plan = normalizeVerificationPlan(parsed);
285
+ if (found !== undefined) {
286
+ throw new Error(`Project ${project.id} declares multiple VerificationPlans; only one is allowed.`);
287
+ }
288
+ found = plan;
289
+ }
290
+ return found;
291
+ }
292
+ /** Serialize a plan for storage in a Project knowledge entry body. */
293
+ export function verificationPlanKnowledgeBody(plan) {
294
+ return `${JSON.stringify(plan, null, 2)}\n`;
295
+ }
296
+ function canonicalJson(value) {
297
+ return JSON.stringify(canonicalize(value));
298
+ }
299
+ function canonicalize(value) {
300
+ if (Array.isArray(value))
301
+ return value.map(canonicalize);
302
+ if (typeof value === "object" && value !== null) {
303
+ const entries = Object.entries(value)
304
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
305
+ return Object.fromEntries(entries.map(([key, entry]) => [key, canonicalize(entry)]));
306
+ }
307
+ return value;
308
+ }
@@ -21,13 +21,23 @@ export async function captureManagedGitChanges(input) {
21
21
  const headCommit = await assertManagedHead(input);
22
22
  if (headCommit === input.baseCommit)
23
23
  return null;
24
+ // Rename folding is disabled on purpose: a rename must record its source
25
+ // path as touched and deleted. With rename detection a pure rename would
26
+ // surface only the destination, letting a target that added the same file
27
+ // in parallel (but kept the source) converge on a partial-tree proof.
24
28
  const changedPaths = (await git([
25
29
  "-C", input.path,
26
- "diff", "--name-only", "-z",
30
+ "diff", "--no-renames", "--name-only", "-z",
27
31
  input.baseCommit,
28
32
  headCommit
29
33
  ])).split("\0").filter(Boolean);
30
- return { headCommit, changedPaths };
34
+ const deletedPaths = (await git([
35
+ "-C", input.path,
36
+ "diff", "--no-renames", "--name-only", "-z", "--diff-filter=D",
37
+ input.baseCommit,
38
+ headCommit
39
+ ])).split("\0").filter(Boolean);
40
+ return { headCommit, changedPaths, deletedPaths };
31
41
  }
32
42
  async function assertManagedHead(input) {
33
43
  const currentBranch = (await git([
@@ -1,5 +1,7 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { createWorkItemChangeSet } from "../integration/changeSet.js";
3
+ import { createChangeSetManifest } from "../integration/changeSetManifest.js";
4
+ import { deriveManifestTags } from "../integration/manifestTags.js";
3
5
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
4
6
  import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.js";
5
7
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
@@ -216,6 +218,16 @@ export class WorkItemChangeSetManager {
216
218
  const existing = findWorkItemChangeSet(this.store, context.taskId, context.workItemId, entry.projectId, entry.baseCommit, result.headCommit);
217
219
  if (existing !== undefined)
218
220
  return existing;
221
+ const targetRef = captureTargetRef(this.store, context.taskId, entry.projectId);
222
+ const manifest = createChangeSetManifest({
223
+ tags: deriveManifestTags({
224
+ changedPaths: result.changedPaths,
225
+ deletedPaths: result.deletedPaths
226
+ }),
227
+ deletedPaths: result.deletedPaths,
228
+ ...(targetRef === undefined ? {} : { targetRef }),
229
+ evidenceRefs: this.#captureEvidenceRefs(context.taskId, context.workItemId, result.headCommit)
230
+ });
219
231
  return this.store.transaction((tx) => {
220
232
  assertCaptureStillCurrent(tx, context);
221
233
  const concurrent = findWorkItemChangeSet(tx, context.taskId, context.workItemId, entry.projectId, entry.baseCommit, result.headCommit);
@@ -238,12 +250,46 @@ export class WorkItemChangeSetManager {
238
250
  baseCommit: entry.baseCommit,
239
251
  headCommit: result.headCommit,
240
252
  branch: entry.branch,
241
- changedPaths: result.changedPaths
253
+ changedPaths: result.changedPaths,
254
+ manifest
242
255
  }, nextCaptureTime(tx, context.taskId, context.workItemId, entry.projectId, this.now()));
243
256
  tx.saveChangeSet(context.taskId, changeSet);
244
257
  return changeSet;
245
258
  });
246
259
  }
260
+ /**
261
+ * Verified evidence for the exact reviewed candidate. A completed
262
+ * ReviewRound freezes the candidate it reviewed at `reviewBaseCommit`; when
263
+ * the captured ChangeSet head is that commit, the round's checks are
264
+ * durable evidence for it. "completed" alone is not a verdict: the
265
+ * reviewer may have yielded with failed checks (requires-repair), so only a
266
+ * round with an explicit positive verdict — at least one passed check and
267
+ * no failed one — may feed `evidenceRefs`. The link is by
268
+ * `reviewBaseCommit`, not by `evidenceCommit`: the latter records the exact
269
+ * tree the checks ran on (the frozen base for a clean review, a diagnostic
270
+ * commit for a committed-diagnostic review, absent for a dirty review), and
271
+ * the queue separately re-checks that tree binding before waiving a gate.
272
+ */
273
+ #captureEvidenceRefs(taskId, workItemId, headCommit) {
274
+ return this.store.listReviewRounds(taskId)
275
+ .filter((round) => round.status === "completed"
276
+ && round.workItemId === workItemId
277
+ && round.reviewBaseCommit === headCommit
278
+ && hasPositiveReviewVerdict(round.checks))
279
+ .map(({ id }) => `review-round:${id}`);
280
+ }
281
+ }
282
+ /**
283
+ * A ReviewRound carries reusable verification evidence only when its checks
284
+ * state an explicit positive verdict: at least one check passed and none
285
+ * failed. A round without checks, or with only skipped checks, has no
286
+ * positive conclusion; a round with a failed check rejects the candidate.
287
+ * Neither may skip a later gate.
288
+ */
289
+ function hasPositiveReviewVerdict(checks) {
290
+ return checks !== undefined
291
+ && checks.some((check) => check.outcome === "passed")
292
+ && checks.every((check) => check.outcome !== "failed");
247
293
  }
248
294
  function requireCapturableContext(store, taskId, workItemId, taskFinalReviewContract) {
249
295
  const item = store.getWorkItem(taskId, workItemId);
@@ -410,8 +456,7 @@ function writableEntries(workspace) {
410
456
  return workspace.entries.filter(({ access }) => access === "write");
411
457
  }
412
458
  function latestWorkItemChangeSet(store, taskId, workItemId, projectId, commits) {
413
- return store.listChangeSets(taskId).filter((changeSet) => (changeSet.schemaVersion === 2
414
- && changeSet.workItemId === workItemId
459
+ return store.listChangeSets(taskId).filter((changeSet) => (changeSet.workItemId === workItemId
415
460
  && changeSet.projectId === projectId
416
461
  && (commits === undefined
417
462
  || (changeSet.baseCommit === commits.baseCommit
@@ -423,6 +468,18 @@ function findWorkItemChangeSet(store, taskId, workItemId, projectId, baseCommit,
423
468
  headCommit
424
469
  });
425
470
  }
471
+ /**
472
+ * The intended integration target for a captured Project change: the Task
473
+ * main worktree branch for that Project, matching the Integration default.
474
+ * Returns undefined when the Task main worktree is not ready yet.
475
+ */
476
+ function captureTargetRef(store, taskId, projectId) {
477
+ const mainWorkspace = store.getTaskWorkspace(taskId);
478
+ if (mainWorkspace === null)
479
+ return undefined;
480
+ const entry = mainWorkspace.entries.find((candidate) => candidate.projectId === projectId);
481
+ return entry?.branch;
482
+ }
426
483
  function compareNewestFirst(left, right) {
427
484
  return right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id);
428
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -100,6 +100,14 @@ unavailable external fact that blocks progress.
100
100
 
101
101
  ## Lead with judgment
102
102
 
103
+ - `yui task next-action <task-id>` is decision support, not an autopilot. It
104
+ reads durable Task records and returns one recommended command, exact refs,
105
+ legitimate alternatives, and any `judgmentRequired` explanation. The Leader
106
+ still owns product priority, acceptance, risk, and the choice among legal
107
+ alternatives. Treat protocol inconsistencies, active Run ownership, open
108
+ InputRequests, Draft activation, exact duplicates, and durable final-review
109
+ contracts as hard boundaries; treat semantic-budget and suspected-duplicate
110
+ warnings as evidence rather than commands.
103
111
  - Keep the context layers distinct in every handoff: Yui Core supplies durable
104
112
  identity, lifecycle, access, workspace, and exact-yield safety; this generic
105
113
  role Skill supplies portable collaboration behavior; the bound Project's