@zq-silk/yui 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +25 -11
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +30 -9
- package/dist/commands/taskIntegrationCommands.js +5 -16
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +51 -39
- package/dist/controller/clientRuntime.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +15 -79
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobCleanupInspection.js +67 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +86 -80
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/repository/taskWorkspaceCoordinator.js +72 -145
- package/dist/repository/taskWorkspacePreparer.js +39 -43
- package/dist/repository/workspaceCleanupInspection.js +88 -7
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +18 -11
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/integrationRuntimeIsolation.js +21 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskRuntimeIsolation.js +91 -21
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/archiveDiagnostics.js +26 -8
- package/dist/task/archivePreflight.js +26 -9
- package/dist/task/leaderArchive.js +9 -15
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +306 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +114 -9
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/observability/README.md +13 -3
- package/docs/observability/README.zh-CN.md +10 -3
- package/docs/release-workflow.md +53 -0
- package/docs/release-workflow.zh-CN.md +40 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-delivery.md +36 -8
- package/docs/task-delivery.zh-CN.md +24 -7
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +4 -3
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-leader/references/integration.md +20 -0
- package/skills/yui-operator/SKILL.md +4 -0
- package/skills/yui-reviewer/SKILL.md +3 -0
- package/skills/yui-runtime/SKILL.md +4 -0
- package/skills/yui-runtime/references/publication.md +20 -4
- package/skills/yui-worker/SKILL.md +3 -0
- package/dist/web/assets/client/app.js +0 -804
- package/dist/web/assets/client/components.js +0 -418
- package/dist/web/assets/client/detail.js +0 -333
- package/dist/web/assets/client/dock.js +0 -225
- package/dist/web/assets/client/evidence.js +0 -172
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1161
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -63
- package/dist/web/assets/client/sections.js +0 -313
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -169
- package/dist/web/assets/styles/views.js +0 -292
|
@@ -3,19 +3,19 @@ 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";
|
|
6
|
-
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
7
6
|
import { durableJobIdempotencyKey } from "../job/durableJob.js";
|
|
8
7
|
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
8
|
+
import { compatibleGitArguments, supportedGit } from "../repository/gitCompatibility.js";
|
|
9
|
+
import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
|
|
9
10
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
10
11
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
11
12
|
import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
|
|
12
|
-
import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
13
|
+
import { FileTaskWorkspacePreparer, integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
|
|
13
14
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
15
|
+
import { integrationTmuxSocketRoot } from "../storage/homeLayout.js";
|
|
16
|
+
import { defaultIntegrationRuntimeIsolation } from "../runtime/integrationRuntimeIsolation.js";
|
|
16
17
|
import { advanceTaskProjectCommit } from "../task/task.js";
|
|
17
18
|
import { taskIntegrationSettlementCheckout, taskIntegrationTargetCheckout } from "../task/taskAuthority.js";
|
|
18
|
-
import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
19
19
|
import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
|
|
20
20
|
import { touchGateArtifact } from "../verification/gateArtifactStore.js";
|
|
21
21
|
import { beginGateVerification, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
|
|
@@ -370,12 +370,9 @@ export class GitIntegrationService {
|
|
|
370
370
|
throw new Error("Integration changed before cleanup; read its current record.");
|
|
371
371
|
}
|
|
372
372
|
integration = current;
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (activeJob !== undefined) {
|
|
377
|
-
throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
|
|
378
|
-
}
|
|
373
|
+
const managedWorkspace = this.store.getIntegrationWorkspace(integration.taskId, integration.id);
|
|
374
|
+
if (managedWorkspace === null)
|
|
375
|
+
return "missing";
|
|
379
376
|
const task = this.store.getTask(integration.taskId);
|
|
380
377
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
|
|
381
378
|
throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
|
|
@@ -387,42 +384,8 @@ export class GitIntegrationService {
|
|
|
387
384
|
if (taskRepository === undefined) {
|
|
388
385
|
throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
|
|
389
386
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const runtime = this.#runtimePreparation(integration, managedWorkspace);
|
|
393
|
-
this.runtimeIsolation.cleanup(runtime, integration.status === "committed" ? "completion" : "failure");
|
|
394
|
-
}
|
|
395
|
-
const recorded = managedWorkspace?.entries[0];
|
|
396
|
-
const directory = recorded?.directory
|
|
397
|
-
?? task.projectBindings.find(({ projectId }) => projectId === integration.projectId)?.directory;
|
|
398
|
-
if (directory === undefined) {
|
|
399
|
-
throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
|
|
400
|
-
}
|
|
401
|
-
const result = await this.git.removeIntegrationWorktree({
|
|
402
|
-
repositoryPath: taskRepository,
|
|
403
|
-
container: recorded !== undefined
|
|
404
|
-
? dirname(recorded.path)
|
|
405
|
-
: integrationWorkspaceRoot(this.home, task.id, integration.id),
|
|
406
|
-
directory,
|
|
407
|
-
taskSegment: taskWorkspaceRefSegment(task),
|
|
408
|
-
integrationId: integration.id,
|
|
409
|
-
discardChanges: integration.status === "failed"
|
|
410
|
-
});
|
|
411
|
-
if (result !== "dirty") {
|
|
412
|
-
if (managedWorkspace !== null) {
|
|
413
|
-
this.#resourceRegistrar().markWorkspaceDeleted(managedWorkspace);
|
|
414
|
-
}
|
|
415
|
-
await rm(integrationCheckDirectory(this.home, task.id, integration.id), {
|
|
416
|
-
recursive: true,
|
|
417
|
-
force: true
|
|
418
|
-
});
|
|
419
|
-
this.store.removeManagedWorkspace({
|
|
420
|
-
type: "integration-attempt",
|
|
421
|
-
taskId: integration.taskId,
|
|
422
|
-
integrationAttemptId: integration.id
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
return result;
|
|
387
|
+
return await new FileTaskWorkspacePreparer(this.home, this.store, this.git, this.now)
|
|
388
|
+
.cleanupIntegrationWorkspace(task.id, integration.id, { runtimeIsolation: this.runtimeIsolation });
|
|
426
389
|
}
|
|
427
390
|
finally {
|
|
428
391
|
release();
|
|
@@ -1024,26 +987,6 @@ function isNodeCode(error, code) {
|
|
|
1024
987
|
&& "code" in error
|
|
1025
988
|
&& error.code === code;
|
|
1026
989
|
}
|
|
1027
|
-
function defaultIntegrationRuntimeIsolation(home, homeId) {
|
|
1028
|
-
const controlHome = resolve(home);
|
|
1029
|
-
const runtimeRoot = managedIntegrationRuntimeRoot(controlHome);
|
|
1030
|
-
return new FileTaskRuntimeIsolation({
|
|
1031
|
-
// The integration check's provider data/cache/tmp live in a dedicated Home
|
|
1032
|
-
// partition — the same isolation contract every Task runtime obeys — not in
|
|
1033
|
-
// a system-wide `/tmp` root. Only the tmux socket ENDPOINT stays a short
|
|
1034
|
-
// `/tmp` path (see `integrationCheckEnvironment`) for the `sockaddr_un`
|
|
1035
|
-
// budget; ordinary runtime state is now Yui-managed under Home.
|
|
1036
|
-
runtimeRoot,
|
|
1037
|
-
pathLayout: "compact",
|
|
1038
|
-
controlPlane: {
|
|
1039
|
-
yuiHome: controlHome,
|
|
1040
|
-
managedRuntimeRoot: runtimeRoot,
|
|
1041
|
-
controllerSocketPath: controllerSocketPath(homeId),
|
|
1042
|
-
tmuxNamespace: yuiTmuxServerName(controlHome),
|
|
1043
|
-
globalInstallPaths: [process.execPath]
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
990
|
/**
|
|
1048
991
|
* Reuse is enabled only for an immutable installed release. Development
|
|
1049
992
|
* checkouts have no content-addressed release identity, so they return.
|
|
@@ -1106,8 +1049,10 @@ function integrationCheckDirectory(home, taskId, integrationId) {
|
|
|
1106
1049
|
return join(home, "artifacts", "integration-checks", taskId, integrationId);
|
|
1107
1050
|
}
|
|
1108
1051
|
async function git(args) {
|
|
1052
|
+
const program = supportedGit();
|
|
1053
|
+
const invocation = compatibleGitArguments(args);
|
|
1109
1054
|
try {
|
|
1110
|
-
const result = await executeFile(
|
|
1055
|
+
const result = await executeFile(program.path, invocation, {
|
|
1111
1056
|
encoding: "utf8",
|
|
1112
1057
|
maxBuffer: 16 * 1024 * 1024,
|
|
1113
1058
|
timeout: 120_000
|
|
@@ -1241,15 +1186,6 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
|
|
|
1241
1186
|
}
|
|
1242
1187
|
}
|
|
1243
1188
|
async function checkedOutWorktreePaths(repositoryPath, targetRef) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
])).trim();
|
|
1247
|
-
if (porcelain.length === 0)
|
|
1248
|
-
return [];
|
|
1249
|
-
return porcelain.split(/\n\n+/u).flatMap((record) => {
|
|
1250
|
-
const lines = record.split("\n");
|
|
1251
|
-
const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
|
|
1252
|
-
const branch = lines.find((line) => line.startsWith("branch "))?.slice("branch ".length);
|
|
1253
|
-
return path !== undefined && branch === targetRef ? [path] : [];
|
|
1254
|
-
});
|
|
1189
|
+
return (await readGitWorktrees(repositoryPath))
|
|
1190
|
+
.filter(record => record.branch === targetRef).map(record => record.path);
|
|
1255
1191
|
}
|
|
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { gitOutputLine, supportedGit } from "../repository/gitCompatibility.js";
|
|
6
7
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
7
8
|
import { recordIntegrationConflict, requireResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
8
9
|
const execute = promisify(execFile);
|
|
@@ -281,7 +282,7 @@ async function hasExactRefReceipt(path, branch, before, action) {
|
|
|
281
282
|
}
|
|
282
283
|
const actionId = () => `yui-integration-${randomBytes(16).toString("hex")}`;
|
|
283
284
|
async function gitPath(path, name) {
|
|
284
|
-
const value = (await command(path, ["rev-parse", "--git-path", name]))
|
|
285
|
+
const value = gitOutputLine(await command(path, ["rev-parse", "--git-path", name]));
|
|
285
286
|
return isAbsolute(value) ? value : resolve(path, value);
|
|
286
287
|
}
|
|
287
288
|
async function conflicts(path) {
|
|
@@ -300,8 +301,9 @@ async function succeeds(path, args, action) {
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
async function command(path, args, action) {
|
|
304
|
+
const program = supportedGit();
|
|
303
305
|
try {
|
|
304
|
-
return (await execute(
|
|
306
|
+
return (await execute(program.path, [
|
|
305
307
|
"-C", path, "-c", "user.name=Yui", "-c", "user.email=yui@local",
|
|
306
308
|
"-c", "core.editor=true", "-c", "rebase.backend=merge", ...args
|
|
307
309
|
], {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { lstatSync, mkdirSync, rmdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { jobPreviewSocketRoot } from "../storage/homeLayout.js";
|
|
4
|
+
/** An opt-in launch contract, not a second service registry. The Job remains
|
|
5
|
+
* the sole owner of the process, cancellation and exit evidence. */
|
|
6
|
+
export function isHttpPreview(env) {
|
|
7
|
+
return env.YUI_HTTP_PREVIEW === "1";
|
|
8
|
+
}
|
|
9
|
+
export function validateHttpPreview(env, steps) {
|
|
10
|
+
if (env.YUI_HTTP_PREVIEW !== undefined && !isHttpPreview(env)) {
|
|
11
|
+
throw new Error("YUI_HTTP_PREVIEW must be 1 when supplied.");
|
|
12
|
+
}
|
|
13
|
+
if (env.YUI_PREVIEW_SOCKET !== undefined || steps.some(step => step.env?.YUI_PREVIEW_SOCKET !== undefined || step.env?.YUI_HTTP_PREVIEW !== undefined)) {
|
|
14
|
+
throw new Error("Preview environment is Job-scoped; YUI_PREVIEW_SOCKET is assigned by the runner.");
|
|
15
|
+
}
|
|
16
|
+
if (isHttpPreview(env) && steps.length !== 1) {
|
|
17
|
+
throw new Error("An HTTP preview Job must have exactly one foreground service step.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function httpPreviewSocket(artifactDir) {
|
|
21
|
+
return join(jobPreviewSocketRoot(artifactDir), "http.sock");
|
|
22
|
+
}
|
|
23
|
+
export function assertHttpPreviewSocket(artifactDir) {
|
|
24
|
+
const dir = jobPreviewSocketRoot(artifactDir);
|
|
25
|
+
return assertSocketAt(dir);
|
|
26
|
+
}
|
|
27
|
+
function assertSocketAt(dir) {
|
|
28
|
+
const parent = lstatSync(dir);
|
|
29
|
+
const path = join(dir, "http.sock");
|
|
30
|
+
const socket = lstatSync(path);
|
|
31
|
+
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 0o077)
|
|
32
|
+
|| !socket.isSocket() || socket.uid !== parent.uid) {
|
|
33
|
+
throw new Error("Preview endpoint is not an owned Unix socket.");
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
/** Exclusive creation; never adopt or unlink another process's old endpoint.
|
|
38
|
+
* Cleanup checks the exact directory inode and removes no unknown contents. */
|
|
39
|
+
export function prepareHttpPreview(artifactDir) {
|
|
40
|
+
const dir = jobPreviewSocketRoot(artifactDir);
|
|
41
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
42
|
+
const owner = lstatSync(dir);
|
|
43
|
+
return {
|
|
44
|
+
socket: join(dir, "http.sock"),
|
|
45
|
+
cleanup() {
|
|
46
|
+
const current = lstatSync(dir);
|
|
47
|
+
if (!current.isDirectory() || current.dev !== owner.dev || current.ino !== owner.ino) {
|
|
48
|
+
throw new Error(`Preview owner changed; retained endpoint: ${dir}`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const path = assertSocketAt(dir);
|
|
52
|
+
unlinkSync(path);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code !== "ENOENT")
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
rmdirSync(dir);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
2
|
+
import { processGenerationIsLive } from "../core/fileLockOwner.js";
|
|
3
|
+
import { scanProcessPathRefs } from "../resources/liveReferences.js";
|
|
4
|
+
import { inspectTaskRuntimeCleanupClaims, parseTaskRuntimeIsolationDescriptor, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR } from "../runtime/taskRuntimeIsolation.js";
|
|
5
|
+
/** Result uncertainty and physical occupancy are different facts. Reuse the
|
|
6
|
+
* recorded process generation and resource scanner, without rewriting the Job
|
|
7
|
+
* or treating acknowledgement as stop evidence. Every cleanup reads this anew.
|
|
8
|
+
*/
|
|
9
|
+
export function jobCleanupBlocker(job, home, phase = "before-removal") {
|
|
10
|
+
if (job.status === "queued" || job.status === "running")
|
|
11
|
+
return `DurableJob is ${job.status}.`;
|
|
12
|
+
if (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined) {
|
|
13
|
+
return "Unknown Job result requires explicit acknowledgement.";
|
|
14
|
+
}
|
|
15
|
+
if (job.status !== "unknown-needs-attention" && job.startedAt === undefined
|
|
16
|
+
&& job.process === undefined && job.operation.effect === "none")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (job.process === undefined || home === undefined) {
|
|
19
|
+
return "Job has no exact process/Home evidence for physical inspection.";
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
if (processGenerationIsLive(job.process.pid, job.process.startIdentity)) {
|
|
23
|
+
return "The exact Job runner process is still live.";
|
|
24
|
+
}
|
|
25
|
+
// A reusable idle Role can share this workspace. Its exact-owner stop is
|
|
26
|
+
// allowed only after result/runner checks; path references must then be
|
|
27
|
+
// rechecked before any child deletion or ordinary archive commit.
|
|
28
|
+
if (phase === "before-role-stop")
|
|
29
|
+
return undefined;
|
|
30
|
+
// Steps may outlive a killed runner. Their cwd, logs and isolated runtime
|
|
31
|
+
// paths remain protective references; no process is signalled here.
|
|
32
|
+
const paths = [job.workspace, join(resolve(home), "artifacts", "jobs", job.taskId, job.id),
|
|
33
|
+
...["TMPDIR", "XDG_DATA_HOME", "XDG_CACHE_HOME"].flatMap(key => {
|
|
34
|
+
const path = job.env[key];
|
|
35
|
+
return path !== undefined && isAbsolute(path) ? [path] : [];
|
|
36
|
+
})];
|
|
37
|
+
const serialized = job.env[YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR];
|
|
38
|
+
if (serialized !== undefined) {
|
|
39
|
+
const descriptor = parseTaskRuntimeIsolationDescriptor(serialized);
|
|
40
|
+
const owner = descriptor.workspace.owner;
|
|
41
|
+
if (descriptor.taskId !== job.taskId || descriptor.workspace.root !== job.workspace
|
|
42
|
+
|| owner.type !== job.owner.kind
|
|
43
|
+
|| (owner.type === "integration-attempt" && job.owner.kind === "integration-attempt"
|
|
44
|
+
&& owner.integrationAttemptId !== job.owner.integrationAttemptId)
|
|
45
|
+
|| (owner.type === "work-item" && job.owner.kind === "work-item"
|
|
46
|
+
&& owner.workItemId !== job.owner.workItemId)) {
|
|
47
|
+
return "Job runtime descriptor does not match its exact owner/workspace.";
|
|
48
|
+
}
|
|
49
|
+
const claims = inspectTaskRuntimeCleanupClaims(descriptor);
|
|
50
|
+
const uncertain = claims.find(claim => claim.ownership !== "owned" || claim.state !== "inactive");
|
|
51
|
+
if (uncertain !== undefined)
|
|
52
|
+
return `Job runtime cleanup claim is unverified: ${uncertain.id}.`;
|
|
53
|
+
// cwd/fd references follow rename, unlike the recorded environment.
|
|
54
|
+
// Use the same exact claim inventory as deletion, before archive commits.
|
|
55
|
+
paths.push(descriptor.roots.runtime, ...claims.map(claim => claim.id));
|
|
56
|
+
}
|
|
57
|
+
const scan = scanProcessPathRefs(paths);
|
|
58
|
+
if (scan.diagnostics.some(diagnostic => diagnostic.severity === "error")
|
|
59
|
+
|| [...scan.refs.values()].some(refs => refs.length > 0)) {
|
|
60
|
+
return "Job workspace/log/runtime references are still live or unverified.";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return `Job physical inspection is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
package/dist/job/jobRunner.js
CHANGED
|
@@ -12,6 +12,7 @@ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, ut
|
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { jobStepDirectory } from "./stepDirectory.js";
|
|
15
|
+
import { prepareHttpPreview, isHttpPreview } from "./httpPreview.js";
|
|
15
16
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
16
17
|
import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
17
18
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -108,6 +109,7 @@ export async function runDurableJobRunner(specPath) {
|
|
|
108
109
|
let exitCode = 0;
|
|
109
110
|
let signal = null;
|
|
110
111
|
let failedStep;
|
|
112
|
+
let preview;
|
|
111
113
|
try {
|
|
112
114
|
for (let index = 0; index < spec.steps.length; index += 1) {
|
|
113
115
|
if (cancelled || existsSync(cancelPath)) {
|
|
@@ -134,13 +136,16 @@ export async function runDurableJobRunner(specPath) {
|
|
|
134
136
|
const stepEnv = step.env === undefined
|
|
135
137
|
? spec.env
|
|
136
138
|
: { ...spec.env, ...step.env };
|
|
139
|
+
const launchEnv = isHttpPreview(spec.env)
|
|
140
|
+
? { ...stepEnv, YUI_PREVIEW_SOCKET: (preview ??= prepareHttpPreview(artifactDir)).socket }
|
|
141
|
+
: stepEnv;
|
|
137
142
|
// f4: a new process group (detached) lets the runner kill the whole
|
|
138
143
|
// step tree on timeout or cancel, for both argv and shell forms.
|
|
139
144
|
if (step.argv !== undefined) {
|
|
140
145
|
const [file, ...args] = step.argv;
|
|
141
146
|
child = spawn(file, args, {
|
|
142
147
|
cwd: stepCwd,
|
|
143
|
-
env:
|
|
148
|
+
env: launchEnv,
|
|
144
149
|
stdio: ["ignore", logFd, logFd],
|
|
145
150
|
detached: true
|
|
146
151
|
});
|
|
@@ -148,13 +153,14 @@ export async function runDurableJobRunner(specPath) {
|
|
|
148
153
|
else {
|
|
149
154
|
child = spawn("/bin/sh", ["-lc", step.command], {
|
|
150
155
|
cwd: stepCwd,
|
|
151
|
-
env:
|
|
156
|
+
env: launchEnv,
|
|
152
157
|
stdio: ["ignore", logFd, logFd],
|
|
153
158
|
detached: true
|
|
154
159
|
});
|
|
155
160
|
}
|
|
156
161
|
}
|
|
157
162
|
catch (error) {
|
|
163
|
+
writeFileSync(logFd, `Unable to start Job step: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
158
164
|
closeSync(logFd);
|
|
159
165
|
completedSteps.push({
|
|
160
166
|
name: step.name,
|
|
@@ -231,6 +237,10 @@ export async function runDurableJobRunner(specPath) {
|
|
|
231
237
|
}
|
|
232
238
|
finally {
|
|
233
239
|
clearInterval(heartbeatTimer);
|
|
240
|
+
// The foreground step has settled. Withdraw only its exact endpoint;
|
|
241
|
+
// existing Job/resource checks still own surviving descendant evidence.
|
|
242
|
+
// Failed socket cleanup retains evidence instead of publishing success.
|
|
243
|
+
preview?.cleanup();
|
|
234
244
|
// f2/rr5: Do NOT remove the signal handlers here. A repeated SIGTERM
|
|
235
245
|
// between the step loop and the exit.json write must still hit the
|
|
236
246
|
// idempotent handler (which ignores it) rather than the default handler
|
|
@@ -59,7 +59,8 @@ const definitions = [
|
|
|
59
59
|
{
|
|
60
60
|
name: "artifact.save", summary: "Save a file artifact by relativePath into the Task's local Git repository; returns a commit-pinned reference.",
|
|
61
61
|
effect: "local-mutation", requiredPermissions: ["task:read"], source: "artifacts.saveArtifactCapability",
|
|
62
|
-
inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" },
|
|
62
|
+
inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" },
|
|
63
|
+
encoding: { enum: ["utf8", "base64"] }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
|
|
63
64
|
outputSchema: { type: "object", required: ["taskId", "commit", "relativePath"] }
|
|
64
65
|
},
|
|
65
66
|
{
|
|
@@ -319,6 +320,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
319
320
|
if (name === "artifact.save")
|
|
320
321
|
return saveArtifactCapability(store.rootDirectory(), taskId, {
|
|
321
322
|
relativePath: params.relativePath, content: params.content,
|
|
323
|
+
...(params.encoding === undefined ? {} : { encoding: params.encoding }),
|
|
322
324
|
...(params.message === undefined ? {} : { message: params.message }),
|
|
323
325
|
...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
|
|
324
326
|
});
|
package/dist/message/message.js
CHANGED
|
@@ -42,6 +42,9 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
|
|
|
42
42
|
: { inputControl: {
|
|
43
43
|
action: context.inputControl.action,
|
|
44
44
|
requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
|
|
45
|
+
...(context.inputControl.expectedSessionId === undefined ? {} : {
|
|
46
|
+
expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
|
|
47
|
+
}),
|
|
45
48
|
...(context.inputControl.expectedTarget === undefined
|
|
46
49
|
? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
|
|
47
50
|
} }),
|
|
@@ -236,6 +239,8 @@ export function validateTaskMessage(message) {
|
|
|
236
239
|
throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
|
|
237
240
|
}
|
|
238
241
|
requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
|
|
242
|
+
if (message.inputControl.expectedSessionId !== undefined)
|
|
243
|
+
requireText(message.inputControl.expectedSessionId, "Message Session");
|
|
239
244
|
if (message.inputControl.expectedTarget !== undefined) {
|
|
240
245
|
requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
|
|
241
246
|
if (message.inputControl.action !== "steer") {
|
|
@@ -317,7 +322,11 @@ export function validateTaskMessage(message) {
|
|
|
317
322
|
throw new Error("A recipient requires an exact WorkItem or ReviewRound.");
|
|
318
323
|
}
|
|
319
324
|
}
|
|
320
|
-
|
|
325
|
+
const selectedSessionNondelivery = message.inputControl?.action === "queue"
|
|
326
|
+
&& message.inputControl.expectedSessionId !== undefined
|
|
327
|
+
&& message.continuation?.runId === undefined
|
|
328
|
+
&& message.continuation?.notDeliveredReason !== undefined;
|
|
329
|
+
if (message.continuation !== undefined && message.recipient?.ownerRunId === undefined && !selectedSessionNondelivery) {
|
|
321
330
|
throw new Error("Message continuation requires an owner Assignment.");
|
|
322
331
|
}
|
|
323
332
|
if (message.continuation?.runId !== undefined) {
|
|
@@ -373,6 +382,9 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
|
|
|
373
382
|
: { inputControl: {
|
|
374
383
|
action: context.inputControl.action,
|
|
375
384
|
requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
|
|
385
|
+
...(context.inputControl.expectedSessionId === undefined ? {} : {
|
|
386
|
+
expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
|
|
387
|
+
}),
|
|
376
388
|
...(context.inputControl.expectedTarget === undefined
|
|
377
389
|
? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
|
|
378
390
|
} }),
|
|
@@ -404,6 +416,8 @@ export function validateGlobalRoleMessage(message) {
|
|
|
404
416
|
throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
|
|
405
417
|
}
|
|
406
418
|
requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
|
|
419
|
+
if (message.inputControl.expectedSessionId !== undefined)
|
|
420
|
+
requireText(message.inputControl.expectedSessionId, "Message Session");
|
|
407
421
|
if (message.inputControl.expectedTarget !== undefined) {
|
|
408
422
|
requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
|
|
409
423
|
if (message.inputControl.action !== "steer") {
|
|
@@ -22,7 +22,10 @@ export function renderRoleLaunchComparison(role, effective) {
|
|
|
22
22
|
permission(binding),
|
|
23
23
|
current.defaultAccess
|
|
24
24
|
];
|
|
25
|
-
}), defaultTableWidth())
|
|
25
|
+
}), defaultTableWidth()) + "\n\nSkill package evidence:\n" + (effective.skillPackages === undefined
|
|
26
|
+
? " Legacy / externally recorded: complete package versions were not recorded."
|
|
27
|
+
: effective.skillPackages.map(skill => ` ${skill.id} @ ${skill.digest}\n ${skill.source.kind}: ${skill.source.path}\n`
|
|
28
|
+
+ ` ${skill.fileCount} files; ${skill.byteSize} bytes; inventory: ${skill.manifestPath}`).join("\n"));
|
|
26
29
|
}
|
|
27
30
|
export function activeRoleSummary(role) {
|
|
28
31
|
const binding = role.agentBindings[role.activeAgentId];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { supportedGit } from "./gitCompatibility.js";
|
|
3
4
|
/**
|
|
4
5
|
* Freeze the exact committed heads of a durable managed Lane workspace at the
|
|
5
6
|
* synchronous runtime-terminalization boundary, before opening the result
|
|
@@ -67,7 +68,7 @@ export function snapshotExecutionLaneWorkspaceSync(store, workspace) {
|
|
|
67
68
|
}
|
|
68
69
|
function git(cwd, args) {
|
|
69
70
|
try {
|
|
70
|
-
return execFileSync(
|
|
71
|
+
return execFileSync(supportedGit().path, args, {
|
|
71
72
|
cwd,
|
|
72
73
|
encoding: "utf8",
|
|
73
74
|
timeout: 5_000,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
4
|
+
// The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
|
|
5
|
+
// implement it by saving/restoring FETCH_HEAD without racing another writer.
|
|
6
|
+
export const MINIMUM_GIT_VERSION = "2.29.0";
|
|
7
|
+
export function gitVersionSupport(output) {
|
|
8
|
+
const match = /^git version (\d+)\.(\d+)(?:\.(\d+))?(?:[.\s-].*)?$/u.exec(output.trim());
|
|
9
|
+
const version = match === null ? "unknown" : `${match[1]}.${match[2]}.${match[3] ?? "0"}`;
|
|
10
|
+
const supported = match !== null && (Number(match[1]) > 2
|
|
11
|
+
|| (Number(match[1]) === 2 && Number(match[2]) >= 29));
|
|
12
|
+
return {
|
|
13
|
+
version, supported,
|
|
14
|
+
detail: `Git ${version}; requires >= ${MINIMUM_GIT_VERSION} (isolated fetch without writing FETCH_HEAD). `
|
|
15
|
+
+ "Read-only version check; repository permissions, transport and server capabilities are not tested."
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let cached;
|
|
19
|
+
/** Pin the executable checked, reusing only an unchanged on-disk identity. */
|
|
20
|
+
export function supportedGit() {
|
|
21
|
+
const selected = resolveExecutable("git", process.env.PATH);
|
|
22
|
+
if (selected === undefined)
|
|
23
|
+
throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
|
|
24
|
+
const path = realpathSync(selected);
|
|
25
|
+
const stat = statSync(path);
|
|
26
|
+
const identity = `${path}:${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
27
|
+
if (cached?.identity === identity)
|
|
28
|
+
return cached;
|
|
29
|
+
const output = execFileSync(path, ["--version"], {
|
|
30
|
+
encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"]
|
|
31
|
+
});
|
|
32
|
+
const support = gitVersionSupport(output);
|
|
33
|
+
if (!support.supported) {
|
|
34
|
+
throw new Error(`${path}: ${support.detail} No Git operation was started by this call. `
|
|
35
|
+
+ "Select a supported Git in PATH, run doctor, then retry; existing resources are retained.");
|
|
36
|
+
}
|
|
37
|
+
cached = { identity, path, version: support.version };
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
/** Git paths are relative to the actual -C directory, not the Node process. */
|
|
41
|
+
export function gitOutputLine(output) {
|
|
42
|
+
if (!output.endsWith("\n") || output.length === 1 || output.includes("\0")) {
|
|
43
|
+
throw new Error("Git returned an invalid path or line.");
|
|
44
|
+
}
|
|
45
|
+
// Remove exactly Git's record terminator, never spaces/tabs in a path.
|
|
46
|
+
return output.slice(0, -1);
|
|
47
|
+
}
|
|
48
|
+
/** rev-parse --verify accepts exactly one validated operand on old Git.
|
|
49
|
+
* Keep option-looking data out before removing the newer option terminator.
|
|
50
|
+
*/
|
|
51
|
+
export function compatibleGitArguments(args) {
|
|
52
|
+
const marker = args.indexOf("--end-of-options");
|
|
53
|
+
if (marker < 0)
|
|
54
|
+
return [...args];
|
|
55
|
+
const operand = args[marker + 1];
|
|
56
|
+
if (!args.includes("rev-parse") || !args.includes("--verify")
|
|
57
|
+
|| marker !== args.length - 2 || operand === undefined
|
|
58
|
+
|| operand.length === 0 || operand.startsWith("-") || /[\0\r\n]/u.test(operand)) {
|
|
59
|
+
throw new Error("Unsafe Git revision operand; no Git operation was started.");
|
|
60
|
+
}
|
|
61
|
+
return [...args.slice(0, marker), operand];
|
|
62
|
+
}
|