@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.
- package/README.md +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- 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
|
-
|
|
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.
|
|
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
|
@@ -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
|