@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,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only resource discovery for Resource GC (Issue 10).
|
|
3
|
+
*
|
|
4
|
+
* Discovery enumerates the disk objects Yui created — managed Git worktrees,
|
|
5
|
+
* legacy deployments, and runtime artifacts — and attributes each one to an
|
|
6
|
+
* owner. Only precisely attributable resources become cleanup candidates;
|
|
7
|
+
* anything else is reported and retained.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { basename, join, resolve } from "node:path";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { managedProjectPath } from "../repository/project.js";
|
|
14
|
+
import { isResourceQuarantinePath } from "./resourceRegistry.js";
|
|
15
|
+
import { createResourceRecord, isReleaseNamespacePath, isTerminalTaskStatus } from "./resourceTypes.js";
|
|
16
|
+
const executeFile = promisify(execFile);
|
|
17
|
+
/** Parse `git worktree list --porcelain` output. */
|
|
18
|
+
export function parseGitWorktreePorcelain(output) {
|
|
19
|
+
const entries = [];
|
|
20
|
+
let current = { detached: false, prunable: false };
|
|
21
|
+
for (const line of output.split("\n")) {
|
|
22
|
+
if (line.startsWith("worktree ")) {
|
|
23
|
+
if (current.path !== undefined) {
|
|
24
|
+
entries.push(finalize(current));
|
|
25
|
+
}
|
|
26
|
+
current = { path: line.slice("worktree ".length), detached: false, prunable: false };
|
|
27
|
+
}
|
|
28
|
+
else if (line.startsWith("HEAD ")) {
|
|
29
|
+
current.head = line.slice("HEAD ".length);
|
|
30
|
+
}
|
|
31
|
+
else if (line.startsWith("branch ")) {
|
|
32
|
+
current.branch = line.slice("branch ".length);
|
|
33
|
+
}
|
|
34
|
+
else if (line === "detached") {
|
|
35
|
+
current.detached = true;
|
|
36
|
+
}
|
|
37
|
+
else if (line.startsWith("prunable")) {
|
|
38
|
+
current.prunable = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (current.path !== undefined)
|
|
42
|
+
entries.push(finalize(current));
|
|
43
|
+
return entries;
|
|
44
|
+
function finalize(entry) {
|
|
45
|
+
return {
|
|
46
|
+
path: entry.path,
|
|
47
|
+
...(entry.head === undefined ? {} : { head: entry.head }),
|
|
48
|
+
...(entry.branch === undefined ? {} : { branch: entry.branch }),
|
|
49
|
+
detached: entry.detached,
|
|
50
|
+
prunable: entry.prunable
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function listGitWorktrees(repositoryPath) {
|
|
55
|
+
try {
|
|
56
|
+
const { stdout } = await executeFile("git", ["-C", repositoryPath, "worktree", "list", "--porcelain"], { timeout: 10_000 });
|
|
57
|
+
return parseGitWorktreePorcelain(stdout);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new Error(`Failed to list Git worktrees for ${repositoryPath}: `
|
|
61
|
+
+ `${error instanceof Error ? error.message : "unknown error"}`, { cause: error });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function gitWorktreeCleanliness(path) {
|
|
65
|
+
try {
|
|
66
|
+
const { stdout } = await executeFile("git", ["-C", path, "status", "--porcelain=v1", "--untracked-files=all"], { timeout: 10_000 });
|
|
67
|
+
return stdout.length > 0 ? "dirty" : "clean";
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return "unknown";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Discover every resource Yui created. The scan is read-only and never
|
|
75
|
+
* deletes; classification into releasable/retained happens in the GC engine.
|
|
76
|
+
*/
|
|
77
|
+
export async function discoverResources(input) {
|
|
78
|
+
const home = resolve(input.home);
|
|
79
|
+
const discovered = [];
|
|
80
|
+
// 1. Managed Git worktrees, per Project.
|
|
81
|
+
for (const project of input.projects) {
|
|
82
|
+
const repositoryPath = projectRepositoryPath(home, project);
|
|
83
|
+
if (repositoryPath === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
const worktrees = await listGitWorktrees(repositoryPath);
|
|
86
|
+
for (const worktree of worktrees) {
|
|
87
|
+
if (resolve(worktree.path) === resolve(repositoryPath))
|
|
88
|
+
continue;
|
|
89
|
+
if (isReleaseNamespacePath(home, worktree.path))
|
|
90
|
+
continue;
|
|
91
|
+
if (isResourceQuarantinePath(home, worktree.path))
|
|
92
|
+
continue;
|
|
93
|
+
const owner = attributeWorktreeOwner(home, project, worktree, input.managedWorkspaces);
|
|
94
|
+
// A prunable worktree's gitdir points to a non-existent location; its
|
|
95
|
+
// cleanliness cannot be proven, so it is retained, not released.
|
|
96
|
+
const cleanliness = worktree.prunable
|
|
97
|
+
? "unknown"
|
|
98
|
+
: await gitWorktreeCleanliness(worktree.path);
|
|
99
|
+
const taskStatus = owner.taskId === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: input.taskStatusById.get(owner.taskId);
|
|
102
|
+
discovered.push({
|
|
103
|
+
record: createResourceRecord({
|
|
104
|
+
kind: "worktree",
|
|
105
|
+
path: resolve(worktree.path),
|
|
106
|
+
owner,
|
|
107
|
+
git: {
|
|
108
|
+
repositoryPath,
|
|
109
|
+
...(worktree.branch === undefined ? {} : { branch: worktree.branch }),
|
|
110
|
+
...(worktree.head === undefined ? {} : { head: worktree.head })
|
|
111
|
+
},
|
|
112
|
+
...(sizeOf(worktree.path) === undefined ? {} : { sizeBytes: sizeOf(worktree.path) }),
|
|
113
|
+
cleanliness,
|
|
114
|
+
activeRefs: [],
|
|
115
|
+
disposition: "active"
|
|
116
|
+
}, input.now),
|
|
117
|
+
ownerTerminal: owner.taskId === undefined
|
|
118
|
+
? owner.basis === "unattributed"
|
|
119
|
+
: isTerminalTaskStatus(taskStatus)
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// 2. Legacy deployments (historical runtime; no current-master creator).
|
|
124
|
+
for (const kind of ["deployment", "deployment-backup"]) {
|
|
125
|
+
const directory = kind === "deployment"
|
|
126
|
+
? join(home, "runtime", "deployments")
|
|
127
|
+
: join(home, "runtime", "deploy-backups");
|
|
128
|
+
if (!existsSync(directory))
|
|
129
|
+
continue;
|
|
130
|
+
for (const entry of safeReaddir(directory)) {
|
|
131
|
+
const path = join(directory, entry.name);
|
|
132
|
+
if (isReleaseNamespacePath(home, path))
|
|
133
|
+
continue;
|
|
134
|
+
if (isResourceQuarantinePath(home, path))
|
|
135
|
+
continue;
|
|
136
|
+
const owner = attributeDeploymentOwner(home, entry.name, input.taskStatusById);
|
|
137
|
+
const isGit = existsSync(join(path, ".git"));
|
|
138
|
+
const cleanliness = isGit ? await gitWorktreeCleanliness(path) : "n/a";
|
|
139
|
+
const gitMetadata = isGit ? readDeploymentGitMetadata(path) : undefined;
|
|
140
|
+
const taskStatus = owner.taskId === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: input.taskStatusById.get(owner.taskId);
|
|
143
|
+
discovered.push({
|
|
144
|
+
record: createResourceRecord({
|
|
145
|
+
kind: "deployment",
|
|
146
|
+
path: resolve(path),
|
|
147
|
+
owner,
|
|
148
|
+
...(gitMetadata !== undefined ? { git: gitMetadata } : {}),
|
|
149
|
+
...(sizeOf(path) === undefined ? {} : { sizeBytes: sizeOf(path) }),
|
|
150
|
+
cleanliness,
|
|
151
|
+
activeRefs: [],
|
|
152
|
+
disposition: "active"
|
|
153
|
+
}, input.now),
|
|
154
|
+
ownerTerminal: owner.taskId === undefined
|
|
155
|
+
? false
|
|
156
|
+
: isTerminalTaskStatus(taskStatus)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 3. Runtime artifacts.
|
|
161
|
+
discovered.push(...discoverRuntimeArtifacts(home, input));
|
|
162
|
+
return discovered;
|
|
163
|
+
}
|
|
164
|
+
function discoverRuntimeArtifacts(home, input) {
|
|
165
|
+
const discovered = [];
|
|
166
|
+
const now = input.now;
|
|
167
|
+
// 3a. Exact Task runtime descriptors: runtime/exact-task-runtime/<digest>.json
|
|
168
|
+
const exactDirectory = join(home, "runtime", "exact-task-runtime");
|
|
169
|
+
for (const entry of safeReaddir(exactDirectory)) {
|
|
170
|
+
if (!entry.name.endsWith(".json"))
|
|
171
|
+
continue;
|
|
172
|
+
const path = join(exactDirectory, entry.name);
|
|
173
|
+
const descriptor = readJsonFile(path);
|
|
174
|
+
const taskId = typeof descriptor?.taskId === "string" ? descriptor.taskId : undefined;
|
|
175
|
+
const owner = {
|
|
176
|
+
home,
|
|
177
|
+
...(taskId === undefined ? {} : { taskId }),
|
|
178
|
+
basis: taskId === undefined ? "unattributed" : "descriptor"
|
|
179
|
+
};
|
|
180
|
+
const taskStatus = taskId === undefined ? undefined : input.taskStatusById.get(taskId);
|
|
181
|
+
discovered.push({
|
|
182
|
+
record: createResourceRecord({
|
|
183
|
+
kind: "runtime-artifact",
|
|
184
|
+
path: resolve(path),
|
|
185
|
+
owner,
|
|
186
|
+
...(sizeOf(path) === undefined ? {} : { sizeBytes: sizeOf(path) }),
|
|
187
|
+
cleanliness: "n/a",
|
|
188
|
+
activeRefs: [],
|
|
189
|
+
disposition: "active"
|
|
190
|
+
}, now),
|
|
191
|
+
ownerTerminal: taskId === undefined
|
|
192
|
+
? false
|
|
193
|
+
: isTerminalTaskStatus(taskStatus)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// 3b. Task runtime isolation roots: <home>.task-runtimes/<taskId>/<owner>/<launch>/
|
|
197
|
+
const runtimeRoot = `${resolve(home)}.task-runtimes`;
|
|
198
|
+
if (existsSync(runtimeRoot)) {
|
|
199
|
+
for (const taskEntry of safeReaddir(runtimeRoot)) {
|
|
200
|
+
if (!taskEntry.isDirectory())
|
|
201
|
+
continue;
|
|
202
|
+
const taskId = taskEntry.name;
|
|
203
|
+
const taskRoot = join(runtimeRoot, taskId);
|
|
204
|
+
const taskStatus = input.taskStatusById.get(taskId);
|
|
205
|
+
for (const ownerEntry of safeReaddir(taskRoot)) {
|
|
206
|
+
if (!ownerEntry.isDirectory())
|
|
207
|
+
continue;
|
|
208
|
+
for (const launchEntry of safeReaddir(join(taskRoot, ownerEntry.name))) {
|
|
209
|
+
if (!launchEntry.isDirectory())
|
|
210
|
+
continue;
|
|
211
|
+
const generationRoot = join(taskRoot, ownerEntry.name, launchEntry.name);
|
|
212
|
+
const marker = readTaskRuntimeMarker(generationRoot);
|
|
213
|
+
const owner = marker === undefined
|
|
214
|
+
? { home, taskId, basis: "naming-convention" }
|
|
215
|
+
: {
|
|
216
|
+
home,
|
|
217
|
+
taskId,
|
|
218
|
+
...(marker.workItemId === undefined ? {} : { workItemId: marker.workItemId }),
|
|
219
|
+
...(marker.reviewRoundId === undefined ? {} : { reviewRoundId: marker.reviewRoundId }),
|
|
220
|
+
basis: "marker"
|
|
221
|
+
};
|
|
222
|
+
discovered.push({
|
|
223
|
+
record: createResourceRecord({
|
|
224
|
+
kind: "runtime-artifact",
|
|
225
|
+
path: resolve(generationRoot),
|
|
226
|
+
owner,
|
|
227
|
+
...(sizeOf(generationRoot) === undefined ? {} : { sizeBytes: sizeOf(generationRoot) }),
|
|
228
|
+
cleanliness: "n/a",
|
|
229
|
+
activeRefs: [],
|
|
230
|
+
disposition: "active"
|
|
231
|
+
}, now),
|
|
232
|
+
ownerTerminal: isTerminalTaskStatus(taskStatus)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 3c. Session contexts: hash-named, owner not provable from the filename.
|
|
239
|
+
const sessionContextDirectory = join(home, "runtime", "session-contexts");
|
|
240
|
+
for (const entry of safeReaddir(sessionContextDirectory)) {
|
|
241
|
+
if (!entry.name.endsWith(".md"))
|
|
242
|
+
continue;
|
|
243
|
+
const path = join(sessionContextDirectory, entry.name);
|
|
244
|
+
discovered.push({
|
|
245
|
+
record: createResourceRecord({
|
|
246
|
+
kind: "runtime-artifact",
|
|
247
|
+
path: resolve(path),
|
|
248
|
+
owner: { home, basis: "unattributed" },
|
|
249
|
+
...(sizeOf(path) === undefined ? {} : { sizeBytes: sizeOf(path) }),
|
|
250
|
+
cleanliness: "n/a",
|
|
251
|
+
activeRefs: [],
|
|
252
|
+
disposition: "active"
|
|
253
|
+
}, now),
|
|
254
|
+
ownerTerminal: false
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// 3d. Claude lifecycle plugin: Controller-owned, never auto-released.
|
|
258
|
+
const pluginDirectory = join(home, "runtime", "claude-lifecycle-plugin");
|
|
259
|
+
if (existsSync(pluginDirectory)) {
|
|
260
|
+
discovered.push({
|
|
261
|
+
record: createResourceRecord({
|
|
262
|
+
kind: "runtime-artifact",
|
|
263
|
+
path: resolve(pluginDirectory),
|
|
264
|
+
owner: { home, basis: "descriptor" },
|
|
265
|
+
...(sizeOf(pluginDirectory) === undefined ? {} : { sizeBytes: sizeOf(pluginDirectory) }),
|
|
266
|
+
cleanliness: "n/a",
|
|
267
|
+
activeRefs: [],
|
|
268
|
+
disposition: "active"
|
|
269
|
+
}, now),
|
|
270
|
+
ownerTerminal: false
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return discovered;
|
|
274
|
+
}
|
|
275
|
+
function attributeWorktreeOwner(home, project, worktree, managedWorkspaces) {
|
|
276
|
+
const path = resolve(worktree.path);
|
|
277
|
+
// Durable managed workspace records are the strongest ownership proof.
|
|
278
|
+
for (const workspace of managedWorkspaces) {
|
|
279
|
+
if (resolve(workspace.root) === path
|
|
280
|
+
|| workspace.entries.some((entry) => resolve(entry.path) === path)) {
|
|
281
|
+
return {
|
|
282
|
+
home,
|
|
283
|
+
projectId: project.id,
|
|
284
|
+
taskId: "taskId" in workspace.owner ? workspace.owner.taskId : undefined,
|
|
285
|
+
...(workspace.owner.type === "work-item"
|
|
286
|
+
? { workItemId: workspace.owner.workItemId }
|
|
287
|
+
: {}),
|
|
288
|
+
...(workspace.owner.type === "review-round"
|
|
289
|
+
? { reviewRoundId: workspace.owner.reviewRoundId }
|
|
290
|
+
: {}),
|
|
291
|
+
...(workspace.owner.type === "integration-attempt"
|
|
292
|
+
? { integrationAttemptId: workspace.owner.integrationAttemptId }
|
|
293
|
+
: {}),
|
|
294
|
+
basis: "durable-record"
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Naming convention: a task-N or task-N-<hex> path segment.
|
|
299
|
+
const taskId = extractTaskIdFromPath(path);
|
|
300
|
+
if (taskId !== undefined) {
|
|
301
|
+
return { home, projectId: project.id, taskId, basis: "naming-convention" };
|
|
302
|
+
}
|
|
303
|
+
return { home, projectId: project.id, basis: "unattributed" };
|
|
304
|
+
}
|
|
305
|
+
function attributeDeploymentOwner(home, name, taskStatusById) {
|
|
306
|
+
const taskId = extractTaskIdFromPath(name);
|
|
307
|
+
if (taskId !== undefined && taskStatusById.has(taskId)) {
|
|
308
|
+
return { home, taskId, basis: "naming-convention" };
|
|
309
|
+
}
|
|
310
|
+
return { home, basis: "unattributed" };
|
|
311
|
+
}
|
|
312
|
+
/** Extract a `task-N` id from a path or name segment. */
|
|
313
|
+
export function extractTaskIdFromPath(value) {
|
|
314
|
+
// Match both `task-N` (managed workspace convention) and `taskN` (legacy
|
|
315
|
+
// deployment naming such as `combined-task18-<sha>`).
|
|
316
|
+
const match = value.match(/(?:^|[/_-])(task-?\d+)(?:-[a-f0-9]{8})?(?:[/_$-]|$)/u);
|
|
317
|
+
if (match === null || match[1] === undefined)
|
|
318
|
+
return undefined;
|
|
319
|
+
return match[1].includes("-") ? match[1] : `task-${match[1].slice(4)}`;
|
|
320
|
+
}
|
|
321
|
+
function projectRepositoryPath(home, project) {
|
|
322
|
+
if (project.ownership === "managed") {
|
|
323
|
+
const path = managedProjectPath(home, project.id);
|
|
324
|
+
return existsSync(path) ? path : undefined;
|
|
325
|
+
}
|
|
326
|
+
return existsSync(project.path) ? project.path : undefined;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Read Git metadata for a deployment that is a linked worktree. The .git
|
|
330
|
+
* file points to the worktree's gitdir, which records the common dir and
|
|
331
|
+
* the original branch/head needed for a controlled restore.
|
|
332
|
+
*/
|
|
333
|
+
function readDeploymentGitMetadata(path) {
|
|
334
|
+
try {
|
|
335
|
+
const gitFile = join(path, ".git");
|
|
336
|
+
const gitStat = lstatSync(gitFile);
|
|
337
|
+
let gitDir;
|
|
338
|
+
if (gitStat.isFile()) {
|
|
339
|
+
const content = readFileSync(gitFile, "utf8").trim();
|
|
340
|
+
if (!content.startsWith("gitdir: "))
|
|
341
|
+
return { repositoryPath: path };
|
|
342
|
+
gitDir = content.slice("gitdir: ".length);
|
|
343
|
+
}
|
|
344
|
+
else if (gitStat.isDirectory()) {
|
|
345
|
+
gitDir = gitFile;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
return { repositoryPath: path };
|
|
349
|
+
}
|
|
350
|
+
const resolvedGitDir = resolve(gitDir);
|
|
351
|
+
const commonDirFile = join(resolvedGitDir, "commondir");
|
|
352
|
+
let commonDir;
|
|
353
|
+
if (existsSync(commonDirFile)) {
|
|
354
|
+
const raw = readFileSync(commonDirFile, "utf8").trim();
|
|
355
|
+
commonDir = resolve(resolvedGitDir, raw);
|
|
356
|
+
}
|
|
357
|
+
const headFile = join(resolvedGitDir, "HEAD");
|
|
358
|
+
let head;
|
|
359
|
+
let branch;
|
|
360
|
+
if (existsSync(headFile)) {
|
|
361
|
+
const headContent = readFileSync(headFile, "utf8").trim();
|
|
362
|
+
if (headContent.startsWith("ref: refs/heads/")) {
|
|
363
|
+
branch = headContent.slice("ref: ".length);
|
|
364
|
+
const refPath = join(commonDir ?? resolvedGitDir, headContent.slice("ref: ".length));
|
|
365
|
+
if (existsSync(refPath)) {
|
|
366
|
+
head = readFileSync(refPath, "utf8").trim();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (/^[0-9a-f]{40}$/u.test(headContent)) {
|
|
370
|
+
head = headContent;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
repositoryPath: path,
|
|
375
|
+
...(commonDir === undefined ? {} : { commonDir }),
|
|
376
|
+
...(branch === undefined ? {} : { branch }),
|
|
377
|
+
...(head === undefined ? {} : { head })
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return { repositoryPath: path };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function readTaskRuntimeMarker(generationRoot) {
|
|
385
|
+
try {
|
|
386
|
+
const marker = JSON.parse(readFileSync(join(generationRoot, ".yui-task-runtime-owner.json"), "utf8"));
|
|
387
|
+
const owner = marker.descriptor?.workspace?.owner;
|
|
388
|
+
if (owner === undefined || owner === null)
|
|
389
|
+
return {};
|
|
390
|
+
return {
|
|
391
|
+
...(typeof owner.workItemId === "string" ? { workItemId: owner.workItemId } : {}),
|
|
392
|
+
...(typeof owner.reviewRoundId === "string" ? { reviewRoundId: owner.reviewRoundId } : {})
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function readJsonFile(path) {
|
|
400
|
+
try {
|
|
401
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
402
|
+
return typeof value === "object" && value !== null
|
|
403
|
+
? value
|
|
404
|
+
: undefined;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function safeReaddir(path) {
|
|
411
|
+
try {
|
|
412
|
+
return readdirSync(path, { withFileTypes: true });
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
return Object.freeze([]);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function sizeOf(path) {
|
|
419
|
+
try {
|
|
420
|
+
const stats = statSync(path);
|
|
421
|
+
if (stats.isFile())
|
|
422
|
+
return stats.size;
|
|
423
|
+
return directorySize(path);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function directorySize(path) {
|
|
430
|
+
let total = 0;
|
|
431
|
+
let entries;
|
|
432
|
+
try {
|
|
433
|
+
entries = readdirSync(path, { withFileTypes: true });
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
const child = join(path, entry.name);
|
|
440
|
+
if (entry.isSymbolicLink())
|
|
441
|
+
continue;
|
|
442
|
+
if (entry.isDirectory()) {
|
|
443
|
+
const size = directorySize(child);
|
|
444
|
+
if (size === undefined)
|
|
445
|
+
return undefined;
|
|
446
|
+
total += size;
|
|
447
|
+
}
|
|
448
|
+
else if (entry.isFile()) {
|
|
449
|
+
try {
|
|
450
|
+
total += statSync(child).size;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return total;
|
|
458
|
+
}
|
|
459
|
+
export function resourceKindLabel(kind) {
|
|
460
|
+
return kind === "worktree"
|
|
461
|
+
? "worktree"
|
|
462
|
+
: kind === "deployment"
|
|
463
|
+
? "deployment"
|
|
464
|
+
: "runtime-artifact";
|
|
465
|
+
}
|
|
466
|
+
export function resourceOwnerLabel(owner) {
|
|
467
|
+
if (owner.basis === "unattributed")
|
|
468
|
+
return "unattributed";
|
|
469
|
+
const parts = [
|
|
470
|
+
...(owner.taskId === undefined ? [] : [owner.taskId]),
|
|
471
|
+
...(owner.workItemId === undefined ? [] : [owner.workItemId]),
|
|
472
|
+
...(owner.reviewRoundId === undefined ? [] : [owner.reviewRoundId]),
|
|
473
|
+
...(owner.integrationAttemptId === undefined ? [] : [owner.integrationAttemptId])
|
|
474
|
+
];
|
|
475
|
+
return parts.length === 0 ? owner.basis : parts.join("/");
|
|
476
|
+
}
|
|
477
|
+
export { basename as resourceBasename };
|