@zq-silk/yui 2.11.0 → 2.12.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/dist/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +1 -1
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +7 -2
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +7 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +12 -0
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sqliteSchema.js +77 -6
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +3 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/i18n/en.js +29 -2
- package/dist/web/assets/client/i18n/zh-CN.js +29 -2
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webServer.js +6 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/job-runs.md +392 -17
- package/docs/release-workflow.md +42 -0
- package/docs/release-workflow.zh-CN.md +31 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +27 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const JOBS_SCRIPT = String.raw `
|
|
2
2
|
import { h } from "/assets/js/lib/dom.js";
|
|
3
3
|
import { requestJson, submitMutation } from "/assets/js/lib/api.js";
|
|
4
|
+
import { drawArtifact } from "/assets/js/views/task/files.js";
|
|
4
5
|
|
|
5
6
|
// One view for two server-fixed scopes. Refresh only reads facts; it never
|
|
6
7
|
// admits due work, derives nextDue or replaces a selected exact Run with latest.
|
|
@@ -11,7 +12,17 @@ export function mountJobs(root, t, taskId) {
|
|
|
11
12
|
const list = h("div.jobs-list");
|
|
12
13
|
const detail = h("section.jobs-detail", { "aria-label": copy("details") });
|
|
13
14
|
const editor = h("section");
|
|
14
|
-
|
|
15
|
+
const params = new URLSearchParams(location.search);
|
|
16
|
+
let jobs = [], schedules = [], selected = params.get("job") || undefined,
|
|
17
|
+
selectedRun = params.get("jobRun") || undefined, generation = 0, runGeneration = 0, pending = false, dirty = false;
|
|
18
|
+
const artifactState = {};
|
|
19
|
+
try { artifactState.reference = JSON.parse(params.get("artifact") || "null"); } catch {}
|
|
20
|
+
function pinSelection() {
|
|
21
|
+
const url = new URL(location.href);
|
|
22
|
+
if (selected) url.searchParams.set("job", selected); else url.searchParams.delete("job");
|
|
23
|
+
if (selectedRun) url.searchParams.set("jobRun", selectedRun); else url.searchParams.delete("jobRun");
|
|
24
|
+
history.replaceState(history.state, "", url);
|
|
25
|
+
}
|
|
15
26
|
const blocked = new Set();
|
|
16
27
|
const read = (family, input) => requestJson(base + "/" + family + "?" + new URLSearchParams(input));
|
|
17
28
|
const button = (text, action, primary = false) => h("button.btn" + (primary ? ".btn-primary" : ""), {
|
|
@@ -40,6 +51,11 @@ export function mountJobs(root, t, taskId) {
|
|
|
40
51
|
status(copy("submitting"));
|
|
41
52
|
try {
|
|
42
53
|
const result = await submitMutation(key, base + "/" + family, payload);
|
|
54
|
+
if (result.status === "saved-unregistered") {
|
|
55
|
+
status(copy("registerHelp") + "\n" + result.error, true);
|
|
56
|
+
await refresh(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
43
59
|
status(payload.op === "run"
|
|
44
60
|
? (result.reason === "overlap" ? copy("overlap") : copy("admitted")) + " · " + result.run.id + " · " + result.run.status
|
|
45
61
|
: payload.op === "run-cancel" ? copy("cancelRequested") : copy("saved"));
|
|
@@ -73,39 +89,53 @@ export function mountJobs(root, t, taskId) {
|
|
|
73
89
|
function editJob(job) {
|
|
74
90
|
const id = input(job?.id || "", { required: true, pattern: "[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", disabled: !!job });
|
|
75
91
|
const title = input(job?.title || "", { required: true });
|
|
92
|
+
const kind = choose(taskId ? ["shell", "task", "agent"] : ["builtin", "task", "agent"], job?.configuration.kind);
|
|
93
|
+
kind.disabled = !!job;
|
|
76
94
|
const action = choose(["system.noop", "project.refresh"], job?.configuration.action);
|
|
77
95
|
const project = input(job?.configuration.projectId || "");
|
|
78
96
|
const configuration = h("textarea", { rows: 12, required: true });
|
|
79
97
|
configuration.value = JSON.stringify(job?.configuration || { kind: "shell", owner: { kind: "task" },
|
|
80
98
|
projectId: "", head: "", workspace: "", env: {}, steps: [{ name: "check", command: "true" }] }, null, 2);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
action.
|
|
87
|
-
|
|
99
|
+
const controlled = h("div", null, field(copy("action"), action), field(copy("project"), project), h("p", null, copy("rebind")));
|
|
100
|
+
const configured = h("div", null, h("p", null, copy("taskConfig")), field(copy("configuration"), configuration));
|
|
101
|
+
const fields = [field("ID", id), field(copy("name"), title), field(copy("executor"), kind), controlled, configured];
|
|
102
|
+
const sync = () => {
|
|
103
|
+
controlled.hidden = kind.value !== "builtin"; configured.hidden = !controlled.hidden;
|
|
104
|
+
project.disabled = controlled.hidden || action.value !== "project.refresh"; project.required = !project.disabled;
|
|
105
|
+
configuration.required = !configured.hidden;
|
|
106
|
+
};
|
|
107
|
+
kind.addEventListener("change", () => {
|
|
108
|
+
configuration.value = JSON.stringify(kind.value === "agent"
|
|
109
|
+
? { kind: "agent", target: { kind: "job", agentId: "" }, input: "", concurrency: "serial" }
|
|
110
|
+
: kind.value === "task" ? { kind: "task", goal: "delivery", target: {
|
|
111
|
+
kind: "new", title: "", description: "", projectBindings: [] } }
|
|
112
|
+
: { kind: "shell", owner: { kind: "task" }, projectId: "", head: "", workspace: "", env: {},
|
|
113
|
+
steps: [{ name: "check", command: "true" }] }, null, 2);
|
|
114
|
+
sync();
|
|
115
|
+
});
|
|
116
|
+
action.addEventListener("change", sync); sync();
|
|
88
117
|
form(job ? copy("editJob") : copy("newJob"), fields, () => {
|
|
89
|
-
const config =
|
|
118
|
+
const config = kind.value !== "builtin" ? JSON.parse(configuration.value) : {
|
|
90
119
|
kind: "builtin", action: action.value, input: {},
|
|
91
120
|
...(action.value === "project.refresh" ? { projectId: project.value.trim() } : {})
|
|
92
121
|
};
|
|
93
122
|
void mutate("jobs", { op: job ? "update" : "define", id: id.value.trim(), title: title.value.trim(),
|
|
94
123
|
configuration: config, ...(job ? { expectedRevision: job.revision } : {}) }, result => {
|
|
95
|
-
|
|
124
|
+
selected = result.id; selectedRun = undefined; pinSelection(); closeEditor();
|
|
96
125
|
});
|
|
97
126
|
});
|
|
98
127
|
}
|
|
99
128
|
function editSchedule(schedule) {
|
|
100
129
|
const id = input(schedule?.id || "", { required: true, disabled: !!schedule });
|
|
101
|
-
const jobId = choose(jobs.map(j => j.id), schedule?.jobId || selected);
|
|
102
|
-
|
|
130
|
+
const jobId = choose(jobs.filter(j => !j.builtin || j.id === schedule?.jobId).map(j => j.id), schedule?.jobId || selected);
|
|
131
|
+
jobId.disabled = !!schedule?.builtin;
|
|
132
|
+
const kind = choose(schedule?.builtin ? ["interval", "cron"] : ["once", "interval", "cron"], schedule?.rule.kind || "interval");
|
|
103
133
|
const rule = schedule?.rule;
|
|
104
134
|
const at = input(rule?.at || rule?.anchor || "", { placeholder: "2026-10-17T09:00:00+08:00" });
|
|
105
|
-
const every = input(String(rule?.everyMs || 3600000), { type: "number", min: 1000, step: 1 });
|
|
135
|
+
const every = input(String(rule?.everyMs || 3600000), { type: "number", min: schedule?.builtin ? 60000 : 1000, step: 1 });
|
|
106
136
|
const expression = input(rule?.expression || "0 9 * * *");
|
|
107
137
|
const timezone = input(rule?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
|
|
108
|
-
const when = field(copy("instant"), at), interval = field(copy("every"), every);
|
|
138
|
+
const when = field(copy("instant"), at), interval = field(copy(schedule?.builtin ? "syncEvery" : "every"), every);
|
|
109
139
|
const cron = field(copy("cron"), expression), zone = field(copy("timezone"), timezone);
|
|
110
140
|
const sync = () => {
|
|
111
141
|
when.hidden = kind.value === "cron"; interval.hidden = kind.value !== "interval";
|
|
@@ -118,31 +148,119 @@ export function mountJobs(root, t, taskId) {
|
|
|
118
148
|
const rule = kind.value === "once" ? { kind: "once", at: at.value }
|
|
119
149
|
: kind.value === "interval" ? { kind: "interval", anchor: at.value, everyMs: Number(every.value) }
|
|
120
150
|
: { kind: "cron", expression: expression.value, timezone: timezone.value };
|
|
121
|
-
void mutate("schedules", { op: schedule ? "update" : "create", id: id.value.trim(),
|
|
151
|
+
void mutate("schedules", { op: schedule ? "update" : "create", id: id.value.trim(), rule,
|
|
152
|
+
...(schedule?.builtin ? {} : { jobId: jobId.value, ...(!taskId ? { authorization: "service" } : {}) }),
|
|
122
153
|
...(schedule ? { expectedRevision: schedule.revision } : {}) }, closeEditor);
|
|
123
154
|
});
|
|
124
155
|
}
|
|
125
156
|
async function showRun(jobId, runId, target) {
|
|
157
|
+
const epoch = ++runGeneration;
|
|
126
158
|
selectedRun = runId;
|
|
159
|
+
pinSelection();
|
|
127
160
|
target.replaceChildren(h("p", null, copy("loading")));
|
|
128
161
|
try {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
target.replaceChildren(h("h4", null, run.id), fact(copy("state"), run.status),
|
|
162
|
+
const run = await read("jobs", { op: "run-get", id: jobId, runId });
|
|
163
|
+
if (epoch !== runGeneration || selected !== jobId || selectedRun !== runId) return;
|
|
164
|
+
const evidence = h("section"), artifacts = h("section"), session = h("section");
|
|
165
|
+
target.replaceChildren(h("div.settings-stack", null, h("h4", null, run.id), fact(copy("state"), run.status),
|
|
166
|
+
fact(copy("executor"), run.executor || (run.delegation ? "task" : "process")),
|
|
167
|
+
run.delegation ? h("a.btn", { href: "/?task=" + encodeURIComponent(run.delegation.taskId) },
|
|
168
|
+
copy("executionTask") + " · " + run.delegation.taskId) : null,
|
|
169
|
+
...(run.businessResult ? [h("div", null, fact(copy("result"), run.businessResult.summary),
|
|
170
|
+
raw(copy("result"), run.businessResult))] : []),
|
|
171
|
+
run.cancelRequestedAt ? fact(copy("cancelRequested"), run.cancelRequestedAt) : null,
|
|
133
172
|
fact(copy("trigger"), run.trigger.source + " · " + run.trigger.key),
|
|
134
173
|
fact(copy("created"), run.createdAt), fact(copy("finished"), run.terminalAt),
|
|
135
|
-
raw(copy("snapshot"), run),
|
|
136
|
-
h("h4", null, copy("logs")), logs.length ? h("div", null, logs.map(log => h("section", null,
|
|
137
|
-
h("h5", null, log.step + (log.truncated ? " · " + copy("truncated") : "")),
|
|
138
|
-
h("pre.settings-observation", null, log.text)))) : h("p", null, copy("noLogs")));
|
|
174
|
+
raw(copy("snapshot"), run), artifacts, evidence, session));
|
|
139
175
|
if (["queued", "running", "unknown-needs-attention"].includes(run.status)) target.prepend(
|
|
140
|
-
button(copy("cancelRun"), () =>
|
|
141
|
-
|
|
176
|
+
button(copy("cancelRun"), () => {
|
|
177
|
+
if (window.confirm(copy("cancelBoundary") + "\n" + runId))
|
|
178
|
+
void mutate("jobs", { op: "run-cancel", id: jobId, runId });
|
|
179
|
+
}));
|
|
180
|
+
drawArtifacts(artifacts, jobId, run, epoch);
|
|
181
|
+
if (run.executor === "agent" && run.configuration.target.kind === "job") drawSession(session, jobId, runId);
|
|
182
|
+
try {
|
|
183
|
+
const value = await read("jobs", { op: "run-evidence", id: jobId, runId });
|
|
184
|
+
if (epoch !== runGeneration) return;
|
|
185
|
+
evidence.append(raw(copy("diagnostics"), value));
|
|
186
|
+
if (run.executor !== "agent" && !run.delegation) {
|
|
187
|
+
const logs = await read("jobs", { op: "logs", id: jobId, runId });
|
|
188
|
+
if (epoch !== runGeneration) return;
|
|
189
|
+
evidence.append(h("h4", null, copy("logs")), logs.length ? h("div", null, logs.map(log => h("section", null,
|
|
190
|
+
h("h5", null, log.step + (log.truncated ? " · " + copy("truncated") : "")),
|
|
191
|
+
h("pre.settings-observation", null, log.text)))) : h("p", null, copy("noLogs")));
|
|
192
|
+
}
|
|
193
|
+
} catch (error) { if (epoch === runGeneration) evidence.append(h("p.settings-error", null, copy("diagnostics") + ": " + error.message)); }
|
|
194
|
+
} catch (error) { if (epoch === runGeneration) target.replaceChildren(h("p.settings-error", null, error.message)); }
|
|
195
|
+
}
|
|
196
|
+
function drawArtifacts(target, jobId, run, epoch) {
|
|
197
|
+
const refs = run.artifacts || [];
|
|
198
|
+
const viewer = h("div.file-viewer");
|
|
199
|
+
const readRef = async (ref, offset = 0) => {
|
|
200
|
+
const value = await read("jobs", { op: "artifact-preview", id: jobId, runId: run.id,
|
|
201
|
+
reference: JSON.stringify(ref), offset: String(offset) });
|
|
202
|
+
return { ...value, relativePath: ref.relativePath, commit: ref.version };
|
|
203
|
+
};
|
|
204
|
+
const select = async (ref, offset = 0) => {
|
|
205
|
+
artifactState.reference = ref;
|
|
206
|
+
const url = new URL(location.href);
|
|
207
|
+
url.searchParams.set("artifact", JSON.stringify(ref));
|
|
208
|
+
history.replaceState(history.state, "", url);
|
|
209
|
+
const key = JSON.stringify(ref);
|
|
210
|
+
viewer.replaceChildren(h("p", null, copy("loading")));
|
|
211
|
+
try {
|
|
212
|
+
const value = await readRef(ref, offset);
|
|
213
|
+
if (epoch !== runGeneration || JSON.stringify(artifactState.reference) !== key) return;
|
|
214
|
+
drawArtifact(viewer, value, JSON.stringify(ref.owner), t, {
|
|
215
|
+
compare: false,
|
|
216
|
+
readArtifact: async (_owner, path, commit, page) => {
|
|
217
|
+
const exact = refs.find(r => r.relativePath === path && r.version === commit
|
|
218
|
+
&& JSON.stringify(r.owner) === JSON.stringify(ref.owner));
|
|
219
|
+
if (!exact) throw new Error(copy("registeredOnly"));
|
|
220
|
+
return readRef(exact, page);
|
|
221
|
+
}
|
|
222
|
+
}, (_path, _commit, page) => void select(ref, page), artifactState);
|
|
223
|
+
} catch (error) { if (epoch === runGeneration && JSON.stringify(artifactState.reference) === key)
|
|
224
|
+
viewer.replaceChildren(h("p.settings-error", null, error.message)); }
|
|
225
|
+
};
|
|
226
|
+
target.append(h("h4", null, copy("artifacts")), h("p", null, copy("registeredOnly")),
|
|
227
|
+
...refs.map(ref => button(ref.relativePath + " · " + ref.version.slice(0, 12), () => void select(ref))), viewer);
|
|
228
|
+
if (artifactState.reference && refs.some(ref => JSON.stringify(ref) === JSON.stringify(artifactState.reference)))
|
|
229
|
+
void select(artifactState.reference);
|
|
230
|
+
const recovery = h("div");
|
|
231
|
+
target.append(button(copy("inspectSaved"), async () => {
|
|
232
|
+
recovery.replaceChildren();
|
|
233
|
+
try {
|
|
234
|
+
const result = await read("jobs", { op: "artifact-list", id: jobId, runId: run.id });
|
|
235
|
+
const missing = result.repository.saved.filter(ref => !result.artifacts.some(r => JSON.stringify(r) === JSON.stringify(ref)));
|
|
236
|
+
recovery.append(raw(copy("artifacts"), result), h("p", null, copy("registerHelp")),
|
|
237
|
+
...missing.map(ref => button(copy("register") + " · " + ref.relativePath, () => {
|
|
238
|
+
if (window.confirm(copy("registerHelp") + "\n" + ref.version))
|
|
239
|
+
void mutate("jobs", { op: "artifact-register", id: jobId, runId: run.id, reference: ref });
|
|
240
|
+
})));
|
|
241
|
+
} catch (error) { recovery.append(h("p.settings-error", null, error.message)); }
|
|
242
|
+
}), recovery);
|
|
243
|
+
}
|
|
244
|
+
function drawSession(target, jobId, runId) {
|
|
245
|
+
target.append(button(copy("inspectSession"), async () => {
|
|
246
|
+
try {
|
|
247
|
+
const value = await read("jobs", { op: "session-inspect", id: jobId, runId });
|
|
248
|
+
const set = value.sessions, current = set?.sessions?.[set.activeAgentId];
|
|
249
|
+
target.replaceChildren(raw(copy("inspectSession"), value), h("p", null, copy("sessionBoundary")));
|
|
250
|
+
if (current) target.append(...["session-stop", "session-new"].map(op =>
|
|
251
|
+
button(copy(op === "session-stop" ? "stopSession" : "newSession"), () => {
|
|
252
|
+
if (window.confirm(copy("sessionBoundary") + "\n" + current.nativeSessionId))
|
|
253
|
+
void mutate("jobs", { op, id: jobId, runId, expectedSessionId: current.nativeSessionId });
|
|
254
|
+
})));
|
|
255
|
+
} catch (error) { target.append(h("p.settings-error", null, error.message)); }
|
|
256
|
+
}));
|
|
142
257
|
}
|
|
143
258
|
async function showDetail(epoch) {
|
|
144
259
|
const job = jobs.find(j => j.id === selected);
|
|
145
|
-
if (!job) {
|
|
260
|
+
if (!job) {
|
|
261
|
+
detail.replaceChildren(...(selected ? [h("p.settings-error", null, copy("unavailable") + " · " + selected)] : []));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
146
264
|
const [runs, plans] = await Promise.all([
|
|
147
265
|
read("jobs", { op: "runs", id: job.id }),
|
|
148
266
|
Promise.all(schedules.filter(s => s.jobId === job.id).map(s => read("schedules", { op: "show", id: s.id })))
|
|
@@ -150,38 +268,43 @@ export function mountJobs(root, t, taskId) {
|
|
|
150
268
|
if (epoch !== generation) return;
|
|
151
269
|
runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
152
270
|
const summary = run => run ? (run.id || run.runId) + " · " + run.status + " · "
|
|
153
|
-
+ (run.terminalAt || run.finishedAt || run.createdAt || run.attemptedAt)
|
|
271
|
+
+ (run.terminalAt || run.finishedAt || run.createdAt || run.attemptedAt)
|
|
272
|
+
+ (run.summary || run.businessResult?.summary ? " · " + (run.summary || run.businessResult.summary) : "") : null;
|
|
154
273
|
const runDetail = h("section.job-run");
|
|
155
274
|
detail.replaceChildren(h("div.settings-stack", null, h("h3", null, job.title), fact("ID", job.id),
|
|
156
275
|
fact(copy("scope"), taskId || "global"), fact(copy("project"), job.configuration.projectId),
|
|
276
|
+
job.builtin ? h("p", null, copy("builtinSync")) : null,
|
|
157
277
|
fact(copy("revision"), job.revision),
|
|
278
|
+
fact(copy("executor"), job.configuration.kind),
|
|
158
279
|
fact(copy("recentRun"), summary(runs[0])),
|
|
280
|
+
fact(copy("recentSuccess"), summary(runs.find(run => run.businessResult?.outcome === "succeeded"))),
|
|
159
281
|
job.refresh ? h("div", null,
|
|
160
282
|
fact(copy("refreshAttempt"), summary(job.refresh.lastAttempt)),
|
|
161
283
|
fact(copy("refreshSuccess"), summary(job.refresh.lastSuccessfulRefresh))) : null,
|
|
162
284
|
h("div.settings-actions", null,
|
|
163
285
|
button(copy("runNow"), () => void mutate("jobs", { op: "run", id: job.id, requestId: crypto.randomUUID() }, result => {
|
|
164
|
-
selectedRun = result.run.id;
|
|
286
|
+
selectedRun = result.run.id; pinSelection();
|
|
165
287
|
}), true),
|
|
166
|
-
button(copy("editJob"), () => editJob(job)),
|
|
167
|
-
button(copy("newSchedule"), () => editSchedule())),
|
|
288
|
+
!job.builtin ? button(copy("editJob"), () => editJob(job)) : null,
|
|
289
|
+
!job.builtin && !["agent", "task"].includes(job.configuration.kind) ? button(copy("newSchedule"), () => editSchedule()) : null),
|
|
168
290
|
raw(copy("configuration"), job.configuration),
|
|
169
291
|
job.refresh ? raw(copy("refreshFacts"), job.refresh) : null,
|
|
170
292
|
h("h3", null, copy("schedules")),
|
|
171
293
|
plans.length ? h("div.settings-stack", null, plans.map(({ schedule: s, lastSuccess }) => h("article.settings-card", null,
|
|
172
294
|
h("h4", null, s.id), fact(copy("state"), s.state),
|
|
295
|
+
s.builtin ? fact(copy("automatic"), copy(s.builtin.enabled ? "enabled" : "disabled")) : null,
|
|
173
296
|
s.state === "completed" ? h("p", null, copy("onceCompleted")) : null,
|
|
174
297
|
fact(copy("rule"), s.rule.kind === "cron" ? s.rule.expression : s.rule.kind === "once" ? s.rule.at : s.rule.anchor + " + " + s.rule.everyMs + " ms"),
|
|
175
298
|
fact(copy("timezone"), s.rule.timezone || "UTC / offset"),
|
|
176
|
-
fact(copy("nextDue"), s.nextDue), fact(copy("lastAttempt"), s.lastAttempt
|
|
299
|
+
fact(copy("nextDue"), s.state === "enabled" ? s.nextDue : null), fact(copy("lastAttempt"), s.lastAttempt
|
|
177
300
|
? s.lastAttempt.at + " · " + s.lastAttempt.outcome + " · " + (s.lastAttempt.runId || s.lastAttempt.reason || "") : null),
|
|
178
301
|
fact(copy("lastSuccess"), lastSuccess ? lastSuccess.id + " · " + (lastSuccess.terminalAt || lastSuccess.updatedAt) : null),
|
|
179
302
|
s.reason ? h("p.settings-error", null, s.reason) : null,
|
|
180
303
|
h("div.settings-actions", null,
|
|
181
304
|
button(copy("editSchedule"), () => editSchedule(s)),
|
|
182
|
-
button(copy(s.state === "enabled" ? "disable" : "enable"), () => void mutate("schedules", {
|
|
183
|
-
op: s.state === "enabled" ? "disable" : "enable", id: s.id, expectedRevision: s.revision
|
|
184
|
-
})), s.lastAttempt?.runId ?
|
|
305
|
+
button(copy((s.builtin ? s.builtin.enabled : s.state === "enabled") ? "disable" : "enable"), () => void mutate("schedules", {
|
|
306
|
+
op: (s.builtin ? s.builtin.enabled : s.state === "enabled") ? "disable" : "enable", id: s.id, expectedRevision: s.revision
|
|
307
|
+
})), s.lastAttempt?.runId ? h("a.btn", { href: jobLink(taskId, s.lastAttempt.jobId || job.id, s.lastAttempt.runId) }, copy("inspectRun")) : null),
|
|
185
308
|
raw(copy("details"), s)))) : h("p", null, copy("noSchedules")),
|
|
186
309
|
h("h3", null, copy("history")),
|
|
187
310
|
runs.length ? h("div.jobs-runs", null, runs.map(run =>
|
|
@@ -201,14 +324,14 @@ export function mountJobs(root, t, taskId) {
|
|
|
201
324
|
list.replaceChildren(...(jobs.length ? jobs.map(job => h("button.job-choice", {
|
|
202
325
|
type: "button", "aria-pressed": String(selected === job.id), onclick: () => {
|
|
203
326
|
if (pending || !guardDraft()) return;
|
|
204
|
-
closeEditor(); selected = job.id; selectedRun = undefined; void refresh();
|
|
327
|
+
closeEditor(); selected = job.id; selectedRun = undefined; pinSelection(); void refresh();
|
|
205
328
|
}
|
|
206
329
|
}, h("strong", null, job.title), h("code", null, job.id),
|
|
207
|
-
h("small", null, job.configuration.action || job.configuration.kind),
|
|
330
|
+
h("small", null, job.builtin ? copy("builtinSync") : job.configuration.action || job.configuration.kind),
|
|
208
331
|
h("small", null, job.configuration.projectId || "—"))) : [h("p", null, copy("empty"))]));
|
|
209
332
|
await showDetail(epoch);
|
|
210
333
|
if (announce && epoch === generation) status(copy("observed") + " · " + new Date().toISOString());
|
|
211
|
-
} catch (error) { if (epoch === generation) status(error.message, true); }
|
|
334
|
+
} catch (error) { if (epoch === generation) status(copy("stale") + " " + error.message, true); }
|
|
212
335
|
}
|
|
213
336
|
root.classList.add("jobs-manager");
|
|
214
337
|
root.replaceChildren(h("h2", null, copy("heading")),
|
|
@@ -219,4 +342,10 @@ export function mountJobs(root, t, taskId) {
|
|
|
219
342
|
void refresh();
|
|
220
343
|
return { isDirty: () => dirty, isBusy: () => pending, refresh };
|
|
221
344
|
}
|
|
345
|
+
|
|
346
|
+
export function jobLink(taskId, jobId, runId) {
|
|
347
|
+
const query = new URLSearchParams({ job: jobId, ...(runId ? { jobRun: runId } : {}),
|
|
348
|
+
...(taskId ? { task: taskId, section: "jobs" } : {}) });
|
|
349
|
+
return (taskId ? "/" : "/settings/jobs") + "?" + query;
|
|
350
|
+
}
|
|
222
351
|
`;
|
|
@@ -74,7 +74,7 @@ function artifactViewer(viewer, data, t, actions) {
|
|
|
74
74
|
return { element: viewer, read: read, draw: draw };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
function drawArtifact(viewer, value, taskId, t, actions, read, state = {}) {
|
|
77
|
+
export function drawArtifact(viewer, value, taskId, t, actions, read, state = {}) {
|
|
78
78
|
clear(viewer);
|
|
79
79
|
if (!value) return;
|
|
80
80
|
const selected = value.after || value;
|
|
@@ -121,12 +121,13 @@ function drawArtifact(viewer, value, taskId, t, actions, read, state = {}) {
|
|
|
121
121
|
viewer.append(h("p.viewer-ref", null, mono("sha256:" + selected.digest)));
|
|
122
122
|
if (value.before) viewer.append(h("p.viewer-ref", null, mono("← git:" + value.before.commit + ":" + value.before.relativePath + " · sha256:" + value.before.digest)));
|
|
123
123
|
viewer.append(h("p.faint.small", null, t("evidence.fixedNote")));
|
|
124
|
-
viewer.append(h("div.record-actions", null, value.kind === "image" ? null : copyText, download,
|
|
124
|
+
viewer.append(h("div.record-actions", null, value.kind === "image" ? null : copyText, download,
|
|
125
|
+
actions.openConversation ? feedback : null));
|
|
125
126
|
if (value.kind === "image") { viewer.append(imagePreview(value, t, undefined, preview)); return; }
|
|
126
127
|
const before = h("input", { placeholder: t("materials.before"), "aria-label": t("materials.before"), maxLength: 64 });
|
|
127
128
|
const compare = button(t("materials.compare"), { variant: "ghost" });
|
|
128
129
|
compare.addEventListener("click", function () { read(selected.relativePath, selected.commit, 0, before.value); });
|
|
129
|
-
viewer.append(h("div.record-actions", null, before, compare));
|
|
130
|
+
if (actions.compare !== false) viewer.append(h("div.record-actions", null, before, compare));
|
|
130
131
|
const source = h("pre.code-block.artifact-text", { tabIndex: 0, "aria-label": t("evidence.fixedText") }, value.content);
|
|
131
132
|
if (!value.before && /\.md$|\.markdown$/i.test(selected.relativePath)) {
|
|
132
133
|
viewer.append(markdownPreview(value, taskId, t, actions, preview));
|
|
@@ -207,7 +208,7 @@ function markdownPreview(value, taskId, t, actions, preview) {
|
|
|
207
208
|
feedback.addEventListener("click", function () { actions.openConversation([artifactRef(value), artifactRef(image)]); });
|
|
208
209
|
slot.append(imagePreview(image, t, entry.alt, saved), h("span.viewer-ref", null,
|
|
209
210
|
path + " · git:" + image.commit + " · sha256:" + image.digest),
|
|
210
|
-
h("span.record-actions", null, download, feedback));
|
|
211
|
+
h("span.record-actions", null, download, actions.openConversation ? feedback : null));
|
|
211
212
|
}
|
|
212
213
|
async function observeRead() {
|
|
213
214
|
load.disabled = true; body.dataset.reading = "true"; status.textContent = t("evidence.reading");
|
|
@@ -12,7 +12,7 @@ import { updateObservation } from "/assets/js/views/task/observation.js";
|
|
|
12
12
|
import { renderDelivery } from "/assets/js/views/task/delivery.js";
|
|
13
13
|
import { renderRuntime } from "/assets/js/views/task/runtime.js";
|
|
14
14
|
import { renderRecords } from "/assets/js/views/task/records.js";
|
|
15
|
-
import { mountJobs } from "/assets/js/views/jobs.js";
|
|
15
|
+
import { mountJobs, jobLink } from "/assets/js/views/jobs.js";
|
|
16
16
|
|
|
17
17
|
// Overview: where it stands. Delivery: what it produced. Runtime: what is
|
|
18
18
|
// live now. Records: what happened. Retired section names stay addressable.
|
|
@@ -94,6 +94,10 @@ function dockToggle(mode, iconName, title, t, ctx) {
|
|
|
94
94
|
|
|
95
95
|
function taskMeta(task, t, locale) {
|
|
96
96
|
return h("div.task-meta", null,
|
|
97
|
+
task.source?.kind === "job-run" ? h("a.btn", {
|
|
98
|
+
href: jobLink(task.source.run.scope.kind === "task" ? task.source.run.scope.taskId : undefined,
|
|
99
|
+
task.source.jobId, task.source.run.runId)
|
|
100
|
+
}, t("jobs.sourceJob") + " · " + task.source.jobId + " / " + task.source.run.runId) : null,
|
|
97
101
|
statusBadge(t, "task", "status", task.status),
|
|
98
102
|
h("span", { dataset: { slot: "exec-badge" } }),
|
|
99
103
|
task.priority ? badge(label(t, "priority", task.priority), task.priority === "urgent" || task.priority === "high" ? "accent" : "idle") : null,
|
package/dist/web/taskPreviews.js
CHANGED
|
@@ -25,7 +25,7 @@ export function createTaskPreviews(store, home) {
|
|
|
25
25
|
};
|
|
26
26
|
const registered = (taskId, jobId) => {
|
|
27
27
|
const job = store.getDurableJob(taskId, jobId);
|
|
28
|
-
if (!store.getTask(taskId) || !job || job.taskId !== taskId || !isHttpPreview(job.env)) {
|
|
28
|
+
if (!store.getTask(taskId) || !job || job.delegation || job.taskId !== taskId || !isHttpPreview(job.env)) {
|
|
29
29
|
throw new WebRequestRejected("Registered Task preview not found.");
|
|
30
30
|
}
|
|
31
31
|
validateHttpPreview(job.env, job.steps);
|
|
@@ -69,7 +69,7 @@ export function createTaskPreviews(store, home) {
|
|
|
69
69
|
async list(taskId) {
|
|
70
70
|
if (!store.getTask(taskId))
|
|
71
71
|
throw new WebRequestRejected("Task not found.");
|
|
72
|
-
const jobs = store.listDurableJobs(taskId).filter(job => isHttpPreview(job.env)).reverse();
|
|
72
|
+
const jobs = store.listDurableJobs(taskId).filter((job) => !job.delegation && isHttpPreview(job.env)).reverse();
|
|
73
73
|
const services = await Promise.all(jobs.slice(0, 16).map(async (job) => {
|
|
74
74
|
let current = state(job);
|
|
75
75
|
let detail = job.result?.unknownReason ?? `Job: ${job.status}`;
|
package/dist/web/webJobs.js
CHANGED
|
@@ -5,8 +5,8 @@ import { requireKnownFields } from "../domain/validation.js";
|
|
|
5
5
|
import { WebRequestRejected, webLocalMutation } from "./webMutation.js";
|
|
6
6
|
/** Local-user ingress only, behind the Web token/origin boundary. Never use
|
|
7
7
|
* this adapter for managed RPC callers. The route fixes scope; the server
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* uses public user authority for global controlled actions. Task shell access
|
|
9
|
+
* still resolves the current Operator binding; no invented Session.
|
|
10
10
|
*/
|
|
11
11
|
export function createWebJobs(store, home) {
|
|
12
12
|
function invoke(scope, family, value, write, now) {
|
|
@@ -14,21 +14,31 @@ export function createWebJobs(store, home) {
|
|
|
14
14
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15
15
|
throw new Error("Expected an operation.");
|
|
16
16
|
requireKnownFields(value, family === "jobs"
|
|
17
|
-
? ["op", "id", "title", "configuration", "expectedRevision", "runId", "requestId"
|
|
18
|
-
|
|
17
|
+
? ["op", "id", "title", "configuration", "expectedRevision", "runId", "requestId",
|
|
18
|
+
"reference", "offset", "expectedSessionId"]
|
|
19
|
+
: ["op", "id", "jobId", "rule", "expectedRevision", "authorization", "target"], "Web Job/Schedule request");
|
|
19
20
|
const input = value;
|
|
20
|
-
const reads = family === "jobs" ? ["list", "show", "runs", "run-get", "
|
|
21
|
-
|
|
21
|
+
const reads = family === "jobs" ? ["list", "show", "runs", "run-get", "run-result", "run-evidence",
|
|
22
|
+
"run-delegation", "logs", "artifact-list", "artifact-preview", "session-inspect"] : ["list", "show"];
|
|
23
|
+
const writes = family === "jobs" ? ["define", "update", "run", "run-cancel", "artifact-register",
|
|
24
|
+
"session-stop", "session-new"] : ["create", "update", "enable", "disable"];
|
|
22
25
|
if (!(write ? writes : reads).includes(String(input.op)))
|
|
23
26
|
throw new Error("Operation is not available on this endpoint.");
|
|
24
27
|
if (scope.kind === "task" && !tx.getTask(scope.taskId))
|
|
25
28
|
throw new Error("Task not found.");
|
|
26
|
-
const session = activeLiveRoleAgentSession(tx.getGlobalRoleSessionSet("operator"));
|
|
27
|
-
if (!session)
|
|
28
|
-
throw new Error("A current Operator Session is required for Job management.");
|
|
29
|
-
const caller = { scope: "global", role: "operator", nativeSessionId: session.nativeSessionId,
|
|
30
|
-
agentId: session.agentId, adapterId: session.adapterId };
|
|
29
|
+
const session = scope.kind === "task" ? activeLiveRoleAgentSession(tx.getGlobalRoleSessionSet("operator")) : null;
|
|
30
|
+
if (scope.kind === "task" && !session)
|
|
31
|
+
throw new Error("A current Operator Session is required for Task Job management.");
|
|
32
|
+
const caller = session ? { scope: "global", role: "operator", nativeSessionId: session.nativeSessionId,
|
|
33
|
+
agentId: session.agentId, adapterId: session.adapterId } : { scope: "user" };
|
|
31
34
|
const { requestId, ...fields } = input;
|
|
35
|
+
if (typeof fields.reference === "string")
|
|
36
|
+
fields.reference = JSON.parse(fields.reference);
|
|
37
|
+
if (fields.offset !== undefined) {
|
|
38
|
+
if (!/^\d{1,9}$/u.test(String(fields.offset)))
|
|
39
|
+
throw new Error("Invalid artifact offset.");
|
|
40
|
+
fields.offset = Number(fields.offset);
|
|
41
|
+
}
|
|
32
42
|
if (input.op === "run" && (typeof requestId !== "string" || !requestId.trim() || requestId.length > 200)) {
|
|
33
43
|
throw new Error("Immediate execution requires a stable requestId.");
|
|
34
44
|
}
|
|
@@ -36,6 +46,21 @@ export function createWebJobs(store, home) {
|
|
|
36
46
|
...(input.op === "run" ? { trigger: { source: "manual", key: requestId } } : {}) };
|
|
37
47
|
return (family === "jobs" ? createJobDefinitionControl(tx, home) : createScheduleControl(tx, home))(params, now);
|
|
38
48
|
};
|
|
49
|
+
// Artifact Git I/O is asynchronous, with its own authorization recheck and
|
|
50
|
+
// short publication transaction. Never keep a SQLite transaction across it.
|
|
51
|
+
if (family === "jobs" && value && typeof value === "object"
|
|
52
|
+
&& "op" in value && String(value.op).startsWith("artifact-")) {
|
|
53
|
+
try {
|
|
54
|
+
return Promise.resolve(apply(store)).catch(error => {
|
|
55
|
+
if (!write)
|
|
56
|
+
throw new WebRequestRejected(error instanceof Error ? error.message : String(error));
|
|
57
|
+
throw error; // A Git/publication failure is not proof of non-submission.
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new WebRequestRejected(error instanceof Error ? error.message : String(error));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
39
64
|
if (write)
|
|
40
65
|
return webLocalMutation(store, apply);
|
|
41
66
|
try {
|
package/dist/web/webMaterials.js
CHANGED
|
@@ -30,24 +30,36 @@ async function fixedImage(home, taskId, input) {
|
|
|
30
30
|
/** One selected immutable file, never a workspace path or a remote URL.
|
|
31
31
|
* Markdown is bounded and whole so fences/tables are not split by pagination. */
|
|
32
32
|
export async function readArtifactPreview(home, taskId, ref, offset = 0) {
|
|
33
|
-
if (
|
|
33
|
+
if (ref.taskId !== taskId)
|
|
34
|
+
throw new WebRequestRejected("Materials must belong to the same Task.");
|
|
35
|
+
const valid = validateGitArtifactRef(ref);
|
|
36
|
+
const file = await resolveGitArtifact(home, valid);
|
|
37
|
+
return { ...valid, ...previewArtifactContent(file, offset) };
|
|
38
|
+
}
|
|
39
|
+
/** Shared data-only preview for Task and Job evidence; no path lookup or authority. */
|
|
40
|
+
export function previewArtifactContent(file, offset = 0) {
|
|
41
|
+
if (/\.(svg|gif|avif|bmp|ico|tiff?)$/iu.test(file.relativePath)) {
|
|
34
42
|
throw new WebRequestRejected("Unsupported image format. Save a static PNG, JPEG or WebP instead.");
|
|
35
43
|
}
|
|
36
|
-
if (
|
|
44
|
+
if (file.relativePath.length > 512)
|
|
45
|
+
throw new WebRequestRejected("Artifact path is too long.");
|
|
46
|
+
if (isRasterArtifact(file.relativePath)) {
|
|
37
47
|
if (offset !== 0)
|
|
38
48
|
throw new WebRequestRejected("Images do not have text offsets.");
|
|
39
|
-
return
|
|
49
|
+
return { digest: file.digest, kind: "image", byteSize: file.bytes.length,
|
|
50
|
+
...rasterMetadata(file.bytes), base64: file.bytes.toString("base64") };
|
|
40
51
|
}
|
|
41
|
-
|
|
52
|
+
textPath(file.relativePath);
|
|
53
|
+
const text = decodeText(file.bytes);
|
|
54
|
+
if (/\.md$|\.markdown$/iu.test(file.relativePath)) {
|
|
42
55
|
if (offset !== 0)
|
|
43
56
|
throw new WebRequestRejected("Markdown previews start at offset zero.");
|
|
44
|
-
|
|
45
|
-
if (file.bytes > MAX_MATERIAL_BYTES)
|
|
57
|
+
if (file.bytes.length > MAX_MATERIAL_BYTES)
|
|
46
58
|
throw new WebRequestRejected("Markdown preview exceeds 256 KiB; split the document.");
|
|
47
|
-
return {
|
|
48
|
-
offset: 0, totalCharacters:
|
|
59
|
+
return { digest: file.digest, kind: "markdown", byteSize: file.bytes.length, content: text,
|
|
60
|
+
offset: 0, totalCharacters: text.length, nextOffset: null };
|
|
49
61
|
}
|
|
50
|
-
return
|
|
62
|
+
return { digest: file.digest, byteSize: file.bytes.length, ...page(text, offset) };
|
|
51
63
|
}
|
|
52
64
|
function textPath(path) {
|
|
53
65
|
const safe = safeRelativeArtifactPath(path);
|
package/dist/web/webServer.js
CHANGED
|
@@ -284,15 +284,16 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
284
284
|
let value;
|
|
285
285
|
if (method === "GET") {
|
|
286
286
|
const query = new URL(request.url, "http://localhost").searchParams;
|
|
287
|
-
if ([...query.keys()].some(key => !["op", "id", "runId"].includes(key)
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
if ([...query.keys()].some(key => !["op", "id", "runId", "reference", "offset"].includes(key)
|
|
288
|
+
|| query.getAll(key).length !== 1))
|
|
289
|
+
throw new WebRequestRejected("Unknown or repeated Job query.");
|
|
290
|
+
value = await dependencies.jobs.read(scope, family, Object.fromEntries(query), now());
|
|
290
291
|
}
|
|
291
292
|
else if (method === "POST") {
|
|
292
293
|
if (headerValue(request, "origin") !== `http://${headerValue(request, "host")}`) {
|
|
293
294
|
throw new WebRequestRejected("Job changes require a same-origin request.");
|
|
294
295
|
}
|
|
295
|
-
value = dependencies.jobs.mutate(scope, family, await readMutationBody(request), now());
|
|
296
|
+
value = await dependencies.jobs.mutate(scope, family, await readMutationBody(request), now());
|
|
296
297
|
}
|
|
297
298
|
else
|
|
298
299
|
throw new WebRequestRejected("Method not allowed.");
|
|
@@ -631,7 +632,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
631
632
|
try {
|
|
632
633
|
const assetVersion = new URL(request.url, "http://localhost").searchParams.get("v");
|
|
633
634
|
const asset = findWebAsset(pathname, assetVersion);
|
|
634
|
-
const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|server|connection))?)\/?$/.test(pathname);
|
|
635
|
+
const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|server|connection))?)\/?$/.test(pathname);
|
|
635
636
|
if (pathname === "/" || pathname === "/index.html" || managementPage) {
|
|
636
637
|
sendText(response, 200, "text/html; charset=utf-8", prepareWebDocument(managementPage ? MODELS_HTML.replace("__YUI_WEB_TOKEN__", token) : dashboardHtml(token), managementPage ? "/assets/js/app/manage.js" : "/assets/app.js"), method === "HEAD");
|
|
637
638
|
}
|
|
@@ -53,6 +53,7 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
|
|
|
53
53
|
core: readTaskContext(tx, taskId, environment),
|
|
54
54
|
task: {
|
|
55
55
|
id: task.id, title: clip(task.title), status: task.status, updatedAt: task.updatedAt,
|
|
56
|
+
source: task.source,
|
|
56
57
|
executionGate: task.executionGate, priority: task.priority, tags: task.tags?.slice(0, 16).map(value => clip(value)),
|
|
57
58
|
projectBindings: task.projectBindings.slice(0, 32),
|
|
58
59
|
description: clip(task.description), completionSummary: clip(task.completionSummary),
|