@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
|
@@ -329,6 +329,7 @@ export function buildRunContextPack(store, taskId, runId) {
|
|
|
329
329
|
workspace: run.effective.workspace.root
|
|
330
330
|
}),
|
|
331
331
|
snapshot: contextSnapshotRef(snapshot),
|
|
332
|
+
skillPackages: run.effective.skillPackages ?? null,
|
|
332
333
|
input: run.inputs[0].input,
|
|
333
334
|
authority: Object.freeze({ view, writableProjectIds }),
|
|
334
335
|
pointers,
|
|
@@ -698,7 +699,10 @@ function materialize(layer, store, refId, value) {
|
|
|
698
699
|
refId,
|
|
699
700
|
revision,
|
|
700
701
|
digest,
|
|
701
|
-
summary:
|
|
702
|
+
summary: (store === "project-knowledge"
|
|
703
|
+
? [title, `version: ${record.version}`, ...(record.scope === undefined ? [] : [`scope: ${record.scope}`]),
|
|
704
|
+
...(record.expiresWhen === undefined ? [] : [`expiresWhen: ${record.expiresWhen}`])].join("; ")
|
|
705
|
+
: title).slice(0, 400)
|
|
702
706
|
}),
|
|
703
707
|
value
|
|
704
708
|
});
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { chmodSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
5
|
+
import { validateSkillPackageRef } from "./skillPackage.js";
|
|
5
6
|
import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
6
7
|
export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
7
8
|
/** Portable read pointers accompany every managed input, including the first
|
|
@@ -69,6 +70,13 @@ export function readSessionBootstrapManifest(path) {
|
|
|
69
70
|
requireText(entry.id, "Session Manifest Skill id");
|
|
70
71
|
requireText(entry.path, "Session Manifest Skill path");
|
|
71
72
|
requireDigest(entry.digest, "Session Manifest Skill digest");
|
|
73
|
+
if (entry.package !== undefined) {
|
|
74
|
+
const ref = entry.package;
|
|
75
|
+
validateSkillPackageRef(ref);
|
|
76
|
+
if (entry.id !== ref.id || entry.path !== ref.path || entry.digest !== ref.digest) {
|
|
77
|
+
throw new Error("Session Manifest Skill package identity mismatch.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
72
80
|
}
|
|
73
81
|
const profile = record.roleProfileRef;
|
|
74
82
|
requireDigest(profile.digest, "Session Manifest Role Profile digest");
|
|
@@ -106,7 +114,8 @@ export function materializeSessionBootstrap(input) {
|
|
|
106
114
|
skills: input.skills.map((skill) => Object.freeze({
|
|
107
115
|
id: skill.id,
|
|
108
116
|
path: skill.path,
|
|
109
|
-
digest: digest(skill.content)
|
|
117
|
+
digest: skill.package?.digest ?? digest(skill.content),
|
|
118
|
+
...(skill.package === undefined ? {} : { package: skill.package })
|
|
110
119
|
})),
|
|
111
120
|
roleProfileRef: { digest: profileDigest, path: roleProfilePath },
|
|
112
121
|
contextProtocol: input.owner.scope === "global"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { verifySkillPackage } from "./skillPackage.js";
|
|
6
|
+
import { BUILTIN_YUI_SKILLS } from "./roleSessionContext.js";
|
|
7
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
|
|
8
|
+
const MAX_TEXT = 256 * 1024;
|
|
9
|
+
const digest = (data) => createHash("sha256").update(data).digest("hex");
|
|
10
|
+
/** Read-only source discovery; browsing never snapshots, installs or executes a package. */
|
|
11
|
+
export function skillSource(home, id) {
|
|
12
|
+
if (!SAFE_ID.test(id))
|
|
13
|
+
throw new Error("Invalid Skill id.");
|
|
14
|
+
const builtin = BUILTIN_YUI_SKILLS.has(id);
|
|
15
|
+
return { kind: builtin ? "builtin" : "configured",
|
|
16
|
+
path: builtin ? fileURLToPath(new URL(`../../skills/${id}`, import.meta.url)) : resolve(home, "skills", id) };
|
|
17
|
+
}
|
|
18
|
+
function physical(path) {
|
|
19
|
+
if (realpathSync(path) !== resolve(path))
|
|
20
|
+
throw new Error("Skill paths must not contain symlinks.");
|
|
21
|
+
}
|
|
22
|
+
function textFile(path) {
|
|
23
|
+
physical(path);
|
|
24
|
+
const stat = lstatSync(path);
|
|
25
|
+
if (!stat.isFile() || stat.size > MAX_TEXT)
|
|
26
|
+
throw new Error("Only regular UTF-8 text files up to 256 KiB can be read.");
|
|
27
|
+
const bytes = readFileSync(path);
|
|
28
|
+
if (bytes.length > MAX_TEXT)
|
|
29
|
+
throw new Error("Skill content exceeds 256 KiB.");
|
|
30
|
+
const content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
31
|
+
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/u.test(content))
|
|
32
|
+
throw new Error("Binary Skill resources cannot be displayed.");
|
|
33
|
+
return { content, byteSize: bytes.length, fileDigest: digest(bytes) };
|
|
34
|
+
}
|
|
35
|
+
export function listSkillSources(home, query = "", cursor = "0") {
|
|
36
|
+
if (query.length > 256 || !/^\d{1,8}$/u.test(cursor))
|
|
37
|
+
throw new Error("Invalid Skill search or cursor.");
|
|
38
|
+
let configured = [];
|
|
39
|
+
try {
|
|
40
|
+
const directory = resolve(home, "skills");
|
|
41
|
+
physical(directory);
|
|
42
|
+
configured = readdirSync(directory).filter(id => SAFE_ID.test(id));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error.code !== "ENOENT")
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
// Bounded discovery has an explicit limit, never silently presents a partial catalog.
|
|
49
|
+
if (configured.length > 2048)
|
|
50
|
+
throw new Error("Skill directory exceeds the 2048-entry browsing limit.");
|
|
51
|
+
const needle = query.trim().toLowerCase();
|
|
52
|
+
const items = [...new Set([...BUILTIN_YUI_SKILLS, ...configured])].sort().map(id => {
|
|
53
|
+
const source = skillSource(home, id);
|
|
54
|
+
try {
|
|
55
|
+
const file = textFile(join(source.path, "SKILL.md"));
|
|
56
|
+
const description = /^description:\s*(.+)$/mu.exec(file.content)?.[1]
|
|
57
|
+
?? file.content.split("\n").find(line => line.trim() && !line.startsWith("#")) ?? "";
|
|
58
|
+
return { id, source, description: description.slice(0, 600), error: null };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return { id, source, description: "", error: error instanceof Error ? error.message : String(error) };
|
|
62
|
+
}
|
|
63
|
+
}).filter(item => !needle || `${item.id} ${item.description}`.toLowerCase().includes(needle));
|
|
64
|
+
const offset = Number(cursor);
|
|
65
|
+
return { items: items.slice(offset, offset + 30), total: items.length,
|
|
66
|
+
nextCursor: offset + 30 < items.length ? String(offset + 30) : null };
|
|
67
|
+
}
|
|
68
|
+
export function readSkillSource(home, id, resource = "SKILL.md") {
|
|
69
|
+
const source = skillSource(home, id);
|
|
70
|
+
const files = [];
|
|
71
|
+
let entries = 0;
|
|
72
|
+
function walk(path, depth) {
|
|
73
|
+
if (depth > 32)
|
|
74
|
+
throw new Error("Skill resource depth exceeds 32.");
|
|
75
|
+
physical(path);
|
|
76
|
+
for (const name of readdirSync(path).sort()) {
|
|
77
|
+
if (++entries > 4096)
|
|
78
|
+
throw new Error("Skill exceeds the 4096-entry browsing limit.");
|
|
79
|
+
const child = join(path, name);
|
|
80
|
+
const stat = lstatSync(child);
|
|
81
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && !stat.isFile())
|
|
82
|
+
throw new Error("Skill contains a symlink or special file.");
|
|
83
|
+
if (stat.isDirectory())
|
|
84
|
+
walk(child, depth + 1);
|
|
85
|
+
else
|
|
86
|
+
files.push(relative(source.path, child).split(sep).join("/"));
|
|
87
|
+
if (files.length > 2048)
|
|
88
|
+
throw new Error("Skill contains more than 2048 resources.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
walk(source.path, 0);
|
|
92
|
+
if (!files.includes("SKILL.md"))
|
|
93
|
+
throw new Error("Skill entrypoint SKILL.md is missing.");
|
|
94
|
+
if (!files.includes(resource))
|
|
95
|
+
throw new Error("Resource is not in this Skill package.");
|
|
96
|
+
return { id, source, version: "current-source", resource, files, ...textFile(join(source.path, resource)) };
|
|
97
|
+
}
|
|
98
|
+
/** The reference must come from a stored Session/Run, never from client paths. */
|
|
99
|
+
export function readFrozenSkill(ref, resource) {
|
|
100
|
+
const manifest = verifySkillPackage(ref);
|
|
101
|
+
const root = join(dirname(ref.manifestPath), "files");
|
|
102
|
+
const entrypoint = relative(root, join(ref.path, "SKILL.md")).split(sep).join("/");
|
|
103
|
+
const selected = resource ?? entrypoint;
|
|
104
|
+
if (!manifest.files.some(file => file.path === selected))
|
|
105
|
+
throw new Error("Resource is not in this frozen Skill package.");
|
|
106
|
+
return { id: ref.id, source: ref.source, version: ref.digest, resource: selected,
|
|
107
|
+
files: manifest.files.map(file => file.path), ...textFile(join(root, selected)) };
|
|
108
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
const MAX_FILES = 2048;
|
|
5
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
const MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
8
|
+
/** A package is data: no script, hook or executable is invoked while loading it.
|
|
9
|
+
* Builtin Skills share one tree so their sibling-relative references stay valid. */
|
|
10
|
+
export function snapshotSkillPackage(input) {
|
|
11
|
+
const root = resolve(input.root);
|
|
12
|
+
assertPhysical(root);
|
|
13
|
+
const files = [];
|
|
14
|
+
const contents = new Map();
|
|
15
|
+
let byteSize = 0;
|
|
16
|
+
function walk(directory, depth) {
|
|
17
|
+
if (depth > 32)
|
|
18
|
+
throw new Error("Skill package directory depth exceeds 32.");
|
|
19
|
+
for (const name of readdirSync(directory).sort()) {
|
|
20
|
+
if (name.includes("\\") || /[\x00-\x1f\x7f]/u.test(name)) {
|
|
21
|
+
throw new Error(`Invalid Skill resource name: ${JSON.stringify(name)}.`);
|
|
22
|
+
}
|
|
23
|
+
const path = join(directory, name);
|
|
24
|
+
const stat = lstatSync(path);
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
walk(path, depth + 1);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!stat.isFile())
|
|
30
|
+
throw new Error(`Skill package requires regular files, not symlinks or special files: ${path}.`);
|
|
31
|
+
if (files.length >= MAX_FILES || stat.size > MAX_FILE_BYTES || byteSize + stat.size > MAX_BYTES) {
|
|
32
|
+
throw new Error(`Skill package exceeds limits (${MAX_FILES} files, 8 MiB/file, 32 MiB total).`);
|
|
33
|
+
}
|
|
34
|
+
const bytes = readFileSync(path);
|
|
35
|
+
if (bytes.length !== stat.size)
|
|
36
|
+
throw new Error(`Skill resource changed while reading: ${path}.`);
|
|
37
|
+
byteSize += bytes.length;
|
|
38
|
+
const entry = { path: relative(root, path).split(sep).join("/"), digest: hash(bytes),
|
|
39
|
+
size: bytes.length, executable: (stat.mode & 0o111) !== 0 };
|
|
40
|
+
files.push(entry);
|
|
41
|
+
contents.set(entry.path, bytes);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
walk(root, 0);
|
|
45
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
46
|
+
const subdirectory = input.subdirectory ?? "";
|
|
47
|
+
const entrypoint = subdirectory ? `${subdirectory}/SKILL.md` : "SKILL.md";
|
|
48
|
+
if (!contents.has(entrypoint))
|
|
49
|
+
throw new Error(`Configured Skill not found: ${input.id}.`);
|
|
50
|
+
// Names, raw bytes and executable intent all participate; mtimes and source
|
|
51
|
+
// locations do not. Identical bundles deduplicate across Roles and Sessions.
|
|
52
|
+
const digest = hash(JSON.stringify(files));
|
|
53
|
+
const parent = resolve(input.home, "runtime", "skill-packages");
|
|
54
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
55
|
+
assertPhysical(parent);
|
|
56
|
+
const destination = join(parent, digest);
|
|
57
|
+
const manifest = { schemaVersion: 1, files, digest };
|
|
58
|
+
if (!existsSync(destination)) {
|
|
59
|
+
const staging = mkdtempSync(join(parent, ".staging-"));
|
|
60
|
+
try {
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const path = join(staging, "files", file.path);
|
|
63
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
64
|
+
writeFileSync(path, contents.get(file.path), { flag: "wx", mode: file.executable ? 0o500 : 0o400 });
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(join(staging, "manifest.json"), JSON.stringify(manifest), { flag: "wx", mode: 0o400 });
|
|
67
|
+
try {
|
|
68
|
+
renameSync(staging, destination);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (!existsSync(destination))
|
|
72
|
+
throw error;
|
|
73
|
+
// Another producer can have won the same content-addressed destination.
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(staging, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const ref = {
|
|
81
|
+
id: input.id, path: join(destination, "files", subdirectory), digest,
|
|
82
|
+
source: { kind: input.kind, path: join(root, subdirectory) },
|
|
83
|
+
manifestPath: join(destination, "manifest.json"), fileCount: files.length, byteSize
|
|
84
|
+
};
|
|
85
|
+
verifySkillPackage(ref);
|
|
86
|
+
return ref;
|
|
87
|
+
}
|
|
88
|
+
export function validateSkillPackageRef(ref) {
|
|
89
|
+
if (ref === null || typeof ref !== "object"
|
|
90
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(ref.id)
|
|
91
|
+
|| !/^[a-f0-9]{64}$/u.test(ref.digest)
|
|
92
|
+
|| typeof ref.path !== "string" || !isAbsolute(ref.path)
|
|
93
|
+
|| typeof ref.manifestPath !== "string" || !isAbsolute(ref.manifestPath)
|
|
94
|
+
|| basename(dirname(ref.manifestPath)) !== ref.digest || basename(ref.manifestPath) !== "manifest.json"
|
|
95
|
+
|| !ref.source || !["builtin", "configured"].includes(ref.source.kind)
|
|
96
|
+
|| typeof ref.source.path !== "string" || !isAbsolute(ref.source.path)
|
|
97
|
+
|| !Number.isSafeInteger(ref.fileCount) || ref.fileCount < 1 || ref.fileCount > MAX_FILES
|
|
98
|
+
|| !Number.isSafeInteger(ref.byteSize) || ref.byteSize < 0 || ref.byteSize > MAX_BYTES) {
|
|
99
|
+
throw new Error("Skill package reference is invalid.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Fail closed on lost/corrupt snapshots. Never refill them from mutable sources. */
|
|
103
|
+
export function verifySkillPackage(ref) {
|
|
104
|
+
validateSkillPackageRef(ref);
|
|
105
|
+
const root = join(dirname(ref.manifestPath), "files");
|
|
106
|
+
assertPhysical(dirname(ref.manifestPath));
|
|
107
|
+
assertPhysical(ref.manifestPath);
|
|
108
|
+
if (lstatSync(ref.manifestPath).size > 1024 * 1024)
|
|
109
|
+
throw new Error("Skill package manifest exceeds limits.");
|
|
110
|
+
const manifest = JSON.parse(readFileSync(ref.manifestPath, "utf8"));
|
|
111
|
+
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.files)
|
|
112
|
+
|| manifest.digest !== ref.digest || hash(JSON.stringify(manifest.files)) !== ref.digest
|
|
113
|
+
|| manifest.files.length !== ref.fileCount)
|
|
114
|
+
throw new Error("Skill package manifest digest mismatch.");
|
|
115
|
+
const entrypoint = relative(root, join(ref.path, "SKILL.md")).split(sep).join("/");
|
|
116
|
+
let byteSize = 0;
|
|
117
|
+
const paths = new Set();
|
|
118
|
+
for (const file of manifest.files) {
|
|
119
|
+
if (typeof file.path !== "string" || !file.path || file.path.split("/").some((part) => part === ".." || part === "." || part === "")
|
|
120
|
+
|| file.path.includes("\\") || isAbsolute(file.path) || paths.has(file.path)) {
|
|
121
|
+
throw new Error("Skill package resource path is invalid.");
|
|
122
|
+
}
|
|
123
|
+
paths.add(file.path);
|
|
124
|
+
const path = join(root, file.path);
|
|
125
|
+
assertPhysical(path);
|
|
126
|
+
const stat = lstatSync(path);
|
|
127
|
+
if (!stat.isFile() || stat.size !== file.size || stat.size > MAX_FILE_BYTES
|
|
128
|
+
|| ((stat.mode & 0o111) !== 0) !== file.executable
|
|
129
|
+
|| hash(readFileSync(path)) !== file.digest)
|
|
130
|
+
throw new Error(`Skill package resource changed: ${file.path}.`);
|
|
131
|
+
byteSize += file.size;
|
|
132
|
+
}
|
|
133
|
+
if (!paths.has(entrypoint) || byteSize !== ref.byteSize)
|
|
134
|
+
throw new Error("Skill package entrypoint or size mismatch.");
|
|
135
|
+
let observed = 0;
|
|
136
|
+
function checkInventory(directory, depth) {
|
|
137
|
+
if (depth > 32)
|
|
138
|
+
throw new Error("Skill package directory depth exceeds 32.");
|
|
139
|
+
for (const name of readdirSync(directory)) {
|
|
140
|
+
const path = join(directory, name);
|
|
141
|
+
const stat = lstatSync(path);
|
|
142
|
+
if (stat.isDirectory()) {
|
|
143
|
+
checkInventory(path, depth + 1);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!stat.isFile() || !paths.has(relative(root, path).split(sep).join("/"))) {
|
|
147
|
+
throw new Error("Skill package contains an unrecorded resource.");
|
|
148
|
+
}
|
|
149
|
+
observed++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
checkInventory(root, 0);
|
|
153
|
+
if (observed !== paths.size)
|
|
154
|
+
throw new Error("Skill package inventory changed.");
|
|
155
|
+
return manifest;
|
|
156
|
+
}
|
|
157
|
+
function assertPhysical(path) {
|
|
158
|
+
if (realpathSync(path) !== resolve(path)) {
|
|
159
|
+
throw new Error(`Skill package paths must not contain symlinks: ${path}.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -580,7 +580,8 @@ function freeze(value) {
|
|
|
580
580
|
}
|
|
581
581
|
function summarize(value) {
|
|
582
582
|
const record = value;
|
|
583
|
-
return ["title", "objective", "question", "summary", "body", "status", "type"]
|
|
584
|
-
.flatMap((key) => typeof record[key] === "string"
|
|
583
|
+
return ["title", "scope", "expiresWhen", "version", "objective", "question", "summary", "body", "status", "type"]
|
|
584
|
+
.flatMap((key) => typeof record[key] === "string" || (key === "version" && typeof record[key] === "number")
|
|
585
|
+
? [`${key}: ${record[key]}`] : [])
|
|
585
586
|
.join("; ").slice(0, 400);
|
|
586
587
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
import { TASK_SEARCH_KINDS } from "../storage/taskSearch.js";
|
|
3
|
+
import { contextContentDigest } from "./contextSnapshot.js";
|
|
4
|
+
import { taskCatalogScope } from "./taskCatalog.js";
|
|
5
|
+
import { readTaskContextResource } from "./taskContext.js";
|
|
6
|
+
const PAGE_BYTES = 32 * 1024;
|
|
7
|
+
/** Human-readable field pages, using the same source authorization and version
|
|
8
|
+
* checks as Context. Offsets are Unicode code points, like search snippets. */
|
|
9
|
+
export function readTaskSearchSource(store, taskId, input, environment = {}) {
|
|
10
|
+
if (!input.digest || !Number.isSafeInteger(input.offset) || input.offset < 0) {
|
|
11
|
+
throw usageError("A pinned digest and non-negative source offset are required.");
|
|
12
|
+
}
|
|
13
|
+
const fields = {
|
|
14
|
+
"task-brief": /^\$\.(objective|technicalApproach|currentFocus|leaderSummary|boundaries\[\d+\])$/,
|
|
15
|
+
"task-message": /^\$\.body$/,
|
|
16
|
+
"task-decision": /^\$\.(title|rationale|supersededReason)$/,
|
|
17
|
+
"run": /^\$\.result\.output$/,
|
|
18
|
+
"task": /^\$\.completionSummary$/
|
|
19
|
+
};
|
|
20
|
+
if (!fields[input.store]?.test(input.field))
|
|
21
|
+
throw usageError("Unsupported search source field.");
|
|
22
|
+
const original = readTaskContextResource(store, taskId, input, environment);
|
|
23
|
+
let value = original.value;
|
|
24
|
+
for (const part of input.field.slice(2).replace(/\[(\d+)\]/g, ".$1").split(".")) {
|
|
25
|
+
value = value && typeof value === "object" ? value[part] : undefined;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value !== "string")
|
|
28
|
+
throw usageError("Search source text is unavailable.");
|
|
29
|
+
const points = Array.from(value);
|
|
30
|
+
if (input.offset > points.length)
|
|
31
|
+
throw usageError("Source offset is outside the text.");
|
|
32
|
+
const end = Math.min(input.offset + 4000, points.length);
|
|
33
|
+
return { taskId, ref: original.ref, field: input.field, authority: "reference-only",
|
|
34
|
+
content: points.slice(input.offset, end).join(""), offset: input.offset,
|
|
35
|
+
nextOffset: end < points.length ? end : null, totalCharacters: points.length,
|
|
36
|
+
offsetUnit: "unicode-code-points" };
|
|
37
|
+
}
|
|
38
|
+
export function parseTaskSearchOptions(args) {
|
|
39
|
+
const query = args[0]?.trim();
|
|
40
|
+
if (!query || query.length > 256 || query.includes("\0"))
|
|
41
|
+
throw usageError("Task search requires a query of 1..256 characters.");
|
|
42
|
+
const values = new Map();
|
|
43
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
44
|
+
const key = args[i];
|
|
45
|
+
const value = args[i + 1];
|
|
46
|
+
if (!["--task", "--kind", "--limit", "--cursor"].includes(key) || values.has(key)
|
|
47
|
+
|| value === undefined || value.startsWith("--"))
|
|
48
|
+
throw usageError("Task search expects query [--task <id>] [--kind <kind>] [--limit <1..100>] [--cursor <cursor>].");
|
|
49
|
+
values.set(key, value);
|
|
50
|
+
}
|
|
51
|
+
const limit = values.has("--limit") ? Number(values.get("--limit")) : 20;
|
|
52
|
+
const kind = values.get("--kind");
|
|
53
|
+
const taskId = values.get("--task");
|
|
54
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
55
|
+
throw usageError("Task search limit must be between 1 and 100.");
|
|
56
|
+
if (kind !== undefined && !TASK_SEARCH_KINDS.includes(kind))
|
|
57
|
+
throw usageError("Unknown Task search kind.");
|
|
58
|
+
if (taskId !== undefined && !/^task-[1-9]\d*$/.test(taskId))
|
|
59
|
+
throw usageError("Invalid Task search target.");
|
|
60
|
+
return { query, limit, ...(kind ? { kind } : {}), ...(taskId ? { taskId } : {}),
|
|
61
|
+
...(values.has("--cursor") ? { cursor: values.get("--cursor") } : {}) };
|
|
62
|
+
}
|
|
63
|
+
export function searchTaskBodies(store, options, environment = {}) {
|
|
64
|
+
// Validate typed callers as well as CLI/HTTP callers through one parser.
|
|
65
|
+
const valid = parseTaskSearchOptions([options.query, "--limit", String(options.limit),
|
|
66
|
+
...(options.taskId === undefined ? [] : ["--task", options.taskId]),
|
|
67
|
+
...(options.kind === undefined ? [] : ["--kind", options.kind]),
|
|
68
|
+
...(options.cursor === undefined ? [] : ["--cursor", options.cursor])]);
|
|
69
|
+
return store.readTransaction(reader => {
|
|
70
|
+
const callerTask = taskCatalogScope(reader, environment);
|
|
71
|
+
if (callerTask !== undefined && valid.taskId !== undefined && callerTask !== valid.taskId) {
|
|
72
|
+
throw usageError("Search target is outside the caller's Task.");
|
|
73
|
+
}
|
|
74
|
+
const taskId = callerTask ?? valid.taskId;
|
|
75
|
+
const scope = contextContentDigest({ taskId: taskId ?? null, query: valid.query, kind: valid.kind ?? null });
|
|
76
|
+
const after = valid.cursor === undefined ? undefined : decodeCursor(valid.cursor, scope);
|
|
77
|
+
const rows = reader.queryTaskSearch({ ...valid, taskId, after, limit: valid.limit + 1 });
|
|
78
|
+
const items = [];
|
|
79
|
+
let nextCursor = null;
|
|
80
|
+
const response = () => ({
|
|
81
|
+
query: valid.query, scope: { taskId: taskId ?? null, archived: "included" },
|
|
82
|
+
items, nextCursor, consistency: "current-per-page; repeat search after source changes",
|
|
83
|
+
matching: "literal substring; ASCII case-insensitive; Unicode otherwise exact",
|
|
84
|
+
limits: { maxItems: valid.limit, pageBytes: PAGE_BYTES, snippetCodePoints: 320 },
|
|
85
|
+
reuse: "References are evidence, not execution authority. An authorized reader may supply selected source text and its reference to a new Task; that Task plans and obtains its own authority."
|
|
86
|
+
});
|
|
87
|
+
for (const row of rows.slice(0, valid.limit)) {
|
|
88
|
+
// Reuse the exact Context permission and digest contract for each hit.
|
|
89
|
+
const original = readTaskContextResource(reader, row.taskId, { store: row.store, refId: row.refId }, environment);
|
|
90
|
+
const item = { taskId: row.taskId, kind: row.kind, field: row.field, snippet: row.snippet,
|
|
91
|
+
offset: row.offset, ref: original.ref, authority: "reference-only" };
|
|
92
|
+
const previous = nextCursor;
|
|
93
|
+
nextCursor = encodeCursor(scope, row);
|
|
94
|
+
items.push(item);
|
|
95
|
+
if (Buffer.byteLength(JSON.stringify({ ok: true, data: response() })) > PAGE_BYTES) {
|
|
96
|
+
items.pop();
|
|
97
|
+
nextCursor = previous;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (rows.length === items.length)
|
|
102
|
+
nextCursor = null;
|
|
103
|
+
if (rows.length && !items.length)
|
|
104
|
+
throw usageError("Task search identity exceeds its page budget.");
|
|
105
|
+
return response();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function encodeCursor(scope, row) {
|
|
109
|
+
return Buffer.from(JSON.stringify({ version: 1, scope, after: {
|
|
110
|
+
taskId: row.taskId, kind: row.kind, refId: row.refId, field: row.field
|
|
111
|
+
} })).toString("base64url");
|
|
112
|
+
}
|
|
113
|
+
function decodeCursor(raw, scope) {
|
|
114
|
+
try {
|
|
115
|
+
if (raw.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(raw))
|
|
116
|
+
throw new Error();
|
|
117
|
+
const value = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
118
|
+
if (value.version !== 1 || value.scope !== scope || !value.after
|
|
119
|
+
|| !["taskId", "kind", "refId", "field"].every(key => typeof value.after[key] === "string"
|
|
120
|
+
&& value.after[key].length > 0 && value.after[key].length <= 256))
|
|
121
|
+
throw new Error();
|
|
122
|
+
return value.after;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw usageError("Invalid Task search cursor or cursor belongs to another scope/query.");
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/controlPlaneCli.js
CHANGED
|
@@ -36,7 +36,7 @@ import { runConfigOverview } from "./commands/configOverview.js";
|
|
|
36
36
|
import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
|
|
37
37
|
import { runDurableJobCommand } from "./commands/durableJobCommands.js";
|
|
38
38
|
import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
|
|
39
|
-
import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
|
|
39
|
+
import { runGlobalRoleCommand, previewGlobalRoleAgentConfigurationMutation } from "./commands/globalRoleCommands.js";
|
|
40
40
|
import { runJobCommand } from "./commands/jobCommands.js";
|
|
41
41
|
import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
|
|
42
42
|
import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
|
|
@@ -110,7 +110,7 @@ import { inspectStorageSchema } from "./storage/storageSchema.js";
|
|
|
110
110
|
import { resolveYuiHome } from "./storage/taskStore.js";
|
|
111
111
|
import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
|
|
112
112
|
import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
|
|
113
|
-
import {
|
|
113
|
+
import { archiveOrdinaryTask } from "./task/ordinaryArchive.js";
|
|
114
114
|
import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
|
|
115
115
|
import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
|
|
116
116
|
import { NodeCommandExecutor } from "./tmux/commandExecutor.js";
|
|
@@ -704,7 +704,8 @@ export async function main() {
|
|
|
704
704
|
const domain = resolved[1];
|
|
705
705
|
const roleOptions = {
|
|
706
706
|
yuiHome: home,
|
|
707
|
-
env: process.env
|
|
707
|
+
env: process.env,
|
|
708
|
+
...(validateAgentConfiguration === undefined ? {} : { validateAgentConfiguration })
|
|
708
709
|
};
|
|
709
710
|
if (domain === "show") {
|
|
710
711
|
const result = runConfigOverview(resolved.slice(2), store, process.env, roleOptions);
|
|
@@ -1012,6 +1013,24 @@ export async function main() {
|
|
|
1012
1013
|
emit(renderTaskArchivePreflight(data), false, data);
|
|
1013
1014
|
return;
|
|
1014
1015
|
}
|
|
1016
|
+
if (resolved[1] === "archive-cleanup") {
|
|
1017
|
+
const taskId = resolved[2];
|
|
1018
|
+
if (resolved.length !== 3 || taskId === undefined) {
|
|
1019
|
+
throw usageError("Usage: yui task archive-cleanup <task>.");
|
|
1020
|
+
}
|
|
1021
|
+
taskLocalActor(store, process.env, taskId);
|
|
1022
|
+
const task = store.getTask(taskId);
|
|
1023
|
+
if (task?.status !== "archived")
|
|
1024
|
+
throw usageError("Archive cleanup requires an archived Task.");
|
|
1025
|
+
const archive = taskArchiveDiagnostics(store, task);
|
|
1026
|
+
if (archive.disposition !== "integrated" && archive.disposition !== "abandoned") {
|
|
1027
|
+
throw usageError("Archive has no recorded cleanup disposition; inspect its original event.");
|
|
1028
|
+
}
|
|
1029
|
+
await workspaceCoordinator.cleanupArchivedTask(taskId, archive.disposition);
|
|
1030
|
+
const result = taskArchiveDiagnostics(store, store.getTask(taskId));
|
|
1031
|
+
emit(renderArchiveDiagnostics(result), false, { taskId, ...result });
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1015
1034
|
if (resolved[1] === "artifact") {
|
|
1016
1035
|
// File/directory artifacts live in the Task's local Git repository, so
|
|
1017
1036
|
// their save/read/list are asynchronous and handled here rather than in
|
|
@@ -1021,7 +1040,7 @@ export async function main() {
|
|
|
1021
1040
|
const action = resolved[2];
|
|
1022
1041
|
const taskId = resolved[3];
|
|
1023
1042
|
const usage = "Usage: yui task artifact list <task> | read <task> <relative-path> [<commit>] | "
|
|
1024
|
-
+ "save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]";
|
|
1043
|
+
+ "save <task> <relative-path> <content> [--encoding utf8|base64] [--message <text>] [--expected-head <commit>]";
|
|
1025
1044
|
if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
|
|
1026
1045
|
throw usageError(usage);
|
|
1027
1046
|
}
|
|
@@ -1055,6 +1074,7 @@ export async function main() {
|
|
|
1055
1074
|
const rest = resolved.slice(6);
|
|
1056
1075
|
let message;
|
|
1057
1076
|
let expectedHead;
|
|
1077
|
+
let encoding;
|
|
1058
1078
|
for (let index = 0; index < rest.length; index += 1) {
|
|
1059
1079
|
const value = rest[index + 1];
|
|
1060
1080
|
if (rest[index] === "--message" && value !== undefined) {
|
|
@@ -1067,10 +1087,15 @@ export async function main() {
|
|
|
1067
1087
|
index += 1;
|
|
1068
1088
|
continue;
|
|
1069
1089
|
}
|
|
1090
|
+
if (rest[index] === "--encoding" && (value === "utf8" || value === "base64")) {
|
|
1091
|
+
encoding = value;
|
|
1092
|
+
index += 1;
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1070
1095
|
throw usageError(usage);
|
|
1071
1096
|
}
|
|
1072
1097
|
data = await saveArtifactCapability(home, taskId, {
|
|
1073
|
-
relativePath, content,
|
|
1098
|
+
relativePath, content, ...(encoding === undefined ? {} : { encoding }),
|
|
1074
1099
|
...(message === undefined ? {} : { message }),
|
|
1075
1100
|
...(expectedHead === undefined ? {} : { expectedHead })
|
|
1076
1101
|
});
|
|
@@ -1302,8 +1327,6 @@ export async function main() {
|
|
|
1302
1327
|
});
|
|
1303
1328
|
return;
|
|
1304
1329
|
}
|
|
1305
|
-
let archiveRemoteDeliveryProof;
|
|
1306
|
-
let archiveTaskReviewCandidate;
|
|
1307
1330
|
if (resolved[1] === "archive") {
|
|
1308
1331
|
const request = parseTaskArchiveArguments(resolved.slice(2));
|
|
1309
1332
|
if (request.sourceMessage !== undefined) {
|
|
@@ -1341,32 +1364,23 @@ export async function main() {
|
|
|
1341
1364
|
emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
|
|
1342
1365
|
return;
|
|
1343
1366
|
}
|
|
1367
|
+
if (disposition === "integrated") {
|
|
1368
|
+
const result = await archiveOrdinaryTask(workspaceCoordinator, taskId, {
|
|
1369
|
+
runtime, environment: process.env, yuiHome: home
|
|
1370
|
+
});
|
|
1371
|
+
emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(result)}`, false, result);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1344
1374
|
{
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
if (item?.status !== "accepted" || disposition !== "integrated")
|
|
1355
|
-
continue;
|
|
1356
|
-
try {
|
|
1357
|
-
await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
|
|
1358
|
-
}
|
|
1359
|
-
catch (error) {
|
|
1360
|
-
throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
|
|
1364
|
-
if (cleanup.status === "retained-dirty") {
|
|
1365
|
-
throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
|
|
1366
|
-
}
|
|
1367
|
-
if (cleanup.status === "failed") {
|
|
1368
|
-
throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
|
|
1369
|
-
}
|
|
1375
|
+
await workspaceCoordinator.prepareTaskForArchive(task.id);
|
|
1376
|
+
runTaskCommand(resolved.slice(1), store, {
|
|
1377
|
+
runtime, environment: process.env, yuiHome: home
|
|
1378
|
+
});
|
|
1379
|
+
await workspaceCoordinator.cleanupArchivedTask(task.id, disposition);
|
|
1380
|
+
const current = store.getTask(task.id);
|
|
1381
|
+
const archive = taskArchiveDiagnostics(store, current);
|
|
1382
|
+
emit(`Archived task ${task.id}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
|
|
1383
|
+
return;
|
|
1370
1384
|
}
|
|
1371
1385
|
}
|
|
1372
1386
|
let taskRetirementProof;
|
|
@@ -1520,9 +1534,7 @@ export async function main() {
|
|
|
1520
1534
|
laneDispatchProjectPaths = preparedLanes.projectPaths;
|
|
1521
1535
|
}
|
|
1522
1536
|
const candidateSnapshots = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
|
|
1523
|
-
const actualTaskReviewCandidate =
|
|
1524
|
-
? await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env)
|
|
1525
|
-
: archiveTaskReviewCandidate;
|
|
1537
|
+
const actualTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env);
|
|
1526
1538
|
const deltaRecheckPreflight = await deltaRecheckPreflightForTaskCommand(resolved.slice(1), store, actualTaskReviewCandidate);
|
|
1527
1539
|
// Read-only Host evidence for Session inspect and Role status/list. The command stays
|
|
1528
1540
|
// synchronous over persisted state; this is the live reading it prints
|
|
@@ -1562,9 +1574,6 @@ export async function main() {
|
|
|
1562
1574
|
...(actualTaskReviewCandidate === undefined
|
|
1563
1575
|
? {}
|
|
1564
1576
|
: { actualTaskReviewCandidate }),
|
|
1565
|
-
...(archiveRemoteDeliveryProof === undefined
|
|
1566
|
-
? {}
|
|
1567
|
-
: { archiveRemoteDeliveryProof }),
|
|
1568
1577
|
...(deltaRecheckPreflight === undefined
|
|
1569
1578
|
? {}
|
|
1570
1579
|
: { deltaRecheckPreflight }),
|
|
@@ -2714,7 +2723,10 @@ function selectionPorts(store, catalogs) {
|
|
|
2714
2723
|
}
|
|
2715
2724
|
async function preflightAgentConfigurationMutation(commandArgs, store, catalogs) {
|
|
2716
2725
|
const mutation = profileAgentConfigurationMutation(commandArgs, store)
|
|
2717
|
-
?? taskRoleAgentConfigurationMutation(commandArgs, store)
|
|
2726
|
+
?? taskRoleAgentConfigurationMutation(commandArgs, store)
|
|
2727
|
+
?? (commandArgs[0] === "config" && commandArgs[1] === "role"
|
|
2728
|
+
? previewGlobalRoleAgentConfigurationMutation(commandArgs.slice(2), store, { yuiHome: store.rootDirectory() })
|
|
2729
|
+
: undefined);
|
|
2718
2730
|
if (mutation === undefined) {
|
|
2719
2731
|
await warmConfigurationMutationCatalog(commandArgs, store, catalogs);
|
|
2720
2732
|
return undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { jobCleanupBlocker } from "../job/jobCleanupInspection.js";
|
|
2
3
|
import { describeCliFailure } from "../errors/cliFailure.js";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
|
|
@@ -525,8 +526,8 @@ export class FileTaskWorkflowRuntime {
|
|
|
525
526
|
* live, preserving the owner records for Operator recovery.
|
|
526
527
|
*/
|
|
527
528
|
async assertTaskPhysicalResourcesReleased(taskId) {
|
|
528
|
-
const activeJobs = this.store.
|
|
529
|
-
.filter((job) => job.
|
|
529
|
+
const activeJobs = this.store.listDurableJobs(taskId)
|
|
530
|
+
.filter((job) => jobCleanupBlocker(job, this.home) !== undefined);
|
|
530
531
|
if (activeJobs.length > 0) {
|
|
531
532
|
throw new WorkspaceCleanupBlockedError("physical-resource-live", `task:${taskId}`, true, `Task physical resources are not released: ${activeJobs.length} DurableJob(s) are still active: `
|
|
532
533
|
+ activeJobs.map(({ id }) => id).join(", "));
|