@zq-silk/yui 2.8.1 → 2.10.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 +5 -3
- package/dist/cli/commandCatalog.js +56 -21
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/commands/durableJobCommands.js +50 -0
- package/dist/commands/globalRoleCommands.js +30 -9
- package/dist/commands/inboxCommands.js +79 -22
- package/dist/commands/projectCommands.js +4 -33
- package/dist/commands/scheduleCommands.js +45 -0
- package/dist/commands/taskActivationCommands.js +19 -5
- package/dist/commands/taskCommands.js +8 -2
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskProjectDeliveryCommand.js +56 -0
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
- package/dist/context/runContextPack.js +25 -4
- package/dist/context/runInputContract.js +1 -0
- package/dist/context/taskContext.js +53 -16
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +38 -17
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
- package/dist/controller/globalInputDelivery.js +17 -7
- package/dist/controller/inboxTurnCheck.js +64 -0
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- package/dist/controller/providerRetryAdmission.js +2 -0
- package/dist/controller/runtime.js +11 -2
- package/dist/controller/scheduleControl.js +176 -0
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/external/githubPublicationVerifier.js +14 -5
- package/dist/external/gitlabPublicationVerifier.js +12 -29
- package/dist/grant/taskAuthorization.js +7 -2
- package/dist/interaction/notificationPresentation.js +12 -3
- package/dist/interaction/operatorPresentation.js +5 -0
- package/dist/job/builtinJob.js +58 -0
- package/dist/job/durableJob.js +72 -49
- package/dist/job/jobDefinition.js +68 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/projectRefreshRunner.js +48 -0
- package/dist/message/globalInboxRecovery.js +44 -0
- package/dist/message/globalInterrupt.js +1 -1
- package/dist/message/inboxReceipt.js +14 -6
- package/dist/message/inboxTurn.js +86 -0
- package/dist/message/message.js +11 -0
- package/dist/message/taskInboxOwnership.js +20 -6
- package/dist/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -1
- package/dist/repository/workspaceCleanupInspection.js +6 -1
- package/dist/schedule/schedule.js +108 -0
- package/dist/scheduler/operatorEvent.js +2 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
- package/dist/storage/contextRecords.js +8 -0
- package/dist/storage/jobSchema.js +15 -0
- package/dist/storage/maintenanceCaller.js +1 -1
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +95 -24
- package/dist/storage/sqliteStore.js +123 -10
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
- package/dist/task/archiveDiagnostics.js +1 -1
- package/dist/task/archivePreflight.js +1 -1
- package/dist/task/projectDeliveryScope.js +20 -0
- package/dist/task/publicationToolObservation.js +126 -0
- package/dist/task/remoteDelivery.js +19 -8
- package/dist/task/remoteDeliveryService.js +5 -4
- package/dist/task/taskActivationService.js +35 -0
- package/dist/web/assets/client/app/settings.js +7 -1
- package/dist/web/assets/client/i18n/en.js +65 -2
- package/dist/web/assets/client/i18n/zh-CN.js +65 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/jobs.js +222 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
- package/dist/web/assets/client/views/task/page.js +11 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shared/taskTabs.js +2 -2
- package/dist/web/assets/shell/settingsContent.js +3 -0
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/views/jobs.js +19 -0
- package/dist/web/controllerWeb.js +73 -21
- package/dist/web/taskPreviews.js +21 -13
- package/dist/web/webAccess.js +109 -0
- package/dist/web/webJobs.js +52 -0
- package/dist/web/webServer.js +76 -44
- package/docs/architecture/capabilities-and-resources.md +6 -4
- package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
- package/docs/cli-information-contract.md +57 -4
- package/docs/cli-information-contract.zh-CN.md +41 -2
- package/docs/job-runs.md +224 -0
- package/docs/project-delivery-scope.md +59 -0
- package/docs/release-workflow.md +86 -0
- package/docs/release-workflow.zh-CN.md +68 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +31 -1
- package/docs/storage-baseline.zh-CN.md +25 -1
- package/docs/task-delivery.md +74 -2
- package/docs/task-delivery.zh-CN.md +39 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/i18n/README.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +44 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-operator/SKILL.md +31 -0
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +67 -8
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/context-ordering.md +103 -0
- package/skills/yui-runtime/references/publication.md +44 -4
- package/skills/yui-runtime/references/recovery.md +21 -0
- package/skills/yui-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
|
@@ -40,6 +40,7 @@ import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
|
40
40
|
import { validateTaskBrief } from "../brief/taskBrief.js";
|
|
41
41
|
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
42
42
|
import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
43
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
43
44
|
import { enqueueDurableOperatorFact } from "../scheduler/operatorEvent.js";
|
|
44
45
|
import { validateDurableJob, validDurableJobTransition } from "../job/durableJob.js";
|
|
45
46
|
import { validateGlobalRoleMessage } from "../message/message.js";
|
|
@@ -60,6 +61,8 @@ import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
|
60
61
|
import { operationalTaskRecords, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
|
|
61
62
|
import { writeTelemetryBatch } from "../telemetry/sqliteTelemetryBatch.js";
|
|
62
63
|
import { STORED_RECORD_TABLES, validateStoredRecord } from "./recordValidation.js";
|
|
64
|
+
import { jobScopeKey, validateJobDefinition } from "../job/jobDefinition.js";
|
|
65
|
+
import { validateSchedule } from "../schedule/schedule.js";
|
|
63
66
|
import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
|
|
64
67
|
import { validateWorkItem } from "../workItem/workItem.js";
|
|
65
68
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
@@ -986,10 +989,22 @@ export class SqliteTaskStore {
|
|
|
986
989
|
const params = { name: roleName };
|
|
987
990
|
if (query.handling)
|
|
988
991
|
where.push(`json_extract(payload, '$.inbox.claim') IS NOT NULL
|
|
989
|
-
AND json_extract(payload, '$.inbox.ack') IS NULL
|
|
992
|
+
AND json_extract(payload, '$.inbox.ack') IS NULL
|
|
993
|
+
AND json_extract(payload, '$.inbox.retirement') IS NULL`);
|
|
994
|
+
if (query.unclaimed)
|
|
995
|
+
where.push("json_extract(payload, '$.inbox') IS NULL");
|
|
996
|
+
if (query.handlingExceptSession !== undefined) {
|
|
997
|
+
if (!query.handling)
|
|
998
|
+
throw new Error("A handling Session filter requires a handling query.");
|
|
999
|
+
where.push(`(json_extract(payload, '$.inbox.claim.actor.agentId') != @handlingAgent
|
|
1000
|
+
OR json_extract(payload, '$.inbox.claim.actor.nativeSessionId') != @handlingSession)`);
|
|
1001
|
+
params.handlingAgent = query.handlingExceptSession.agentId;
|
|
1002
|
+
params.handlingSession = query.handlingExceptSession.nativeSessionId;
|
|
1003
|
+
}
|
|
990
1004
|
if (query.pending)
|
|
991
1005
|
where.push(`json_extract(payload, '$.delivery') IS NULL
|
|
992
1006
|
AND json_extract(payload, '$.inbox.ack') IS NULL
|
|
1007
|
+
AND json_extract(payload, '$.inbox.retirement') IS NULL
|
|
993
1008
|
AND json_extract(payload, '$.notDelivered') IS NULL
|
|
994
1009
|
AND (json_extract(payload, '$.inputControl.action') = 'queue'
|
|
995
1010
|
OR json_extract(payload, '$.interruptThen') IS NOT NULL)`);
|
|
@@ -1056,6 +1071,25 @@ export class SqliteTaskStore {
|
|
|
1056
1071
|
throw new StorageRecordError("Task id is required.");
|
|
1057
1072
|
}
|
|
1058
1073
|
this.#mutate(() => {
|
|
1074
|
+
const schedules = this.listSchedules({ kind: "task", taskId: task.id });
|
|
1075
|
+
if (task.status === "completed" && schedules.some(s => s.rule.kind === "once"
|
|
1076
|
+
&& (s.state === "enabled" || s.state === "needs-attention"))) {
|
|
1077
|
+
throw new StorageRecordError("Task has unfulfilled once Schedules; inspect and explicitly disable them before completion.");
|
|
1078
|
+
}
|
|
1079
|
+
if (["completed", "cancelled", "archived"].includes(task.status)) {
|
|
1080
|
+
for (const schedule of schedules.filter(s => s.state === "enabled" || s.state === "needs-attention")) {
|
|
1081
|
+
this.saveSchedule({ ...schedule, state: "retired", revision: schedule.revision + 1,
|
|
1082
|
+
reason: `Task ${task.status}; no future admission. Pending intent and Run history retained.`,
|
|
1083
|
+
updatedAt: task.updatedAt });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
else if (task.executionGate.state === "stopped") {
|
|
1087
|
+
for (const schedule of schedules.filter(s => s.state === "enabled")) {
|
|
1088
|
+
this.saveSchedule({ ...schedule, state: "needs-attention", revision: schedule.revision + 1,
|
|
1089
|
+
reason: "Task execution stopped; inspect and explicitly enable after execution resumes.",
|
|
1090
|
+
updatedAt: task.updatedAt });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1059
1093
|
for (const binding of task.projectBindings) {
|
|
1060
1094
|
const found = this.#db.prepare("SELECT 1 FROM projects WHERE id = ?").get(binding.projectId);
|
|
1061
1095
|
if (found === undefined)
|
|
@@ -1234,6 +1268,71 @@ export class SqliteTaskStore {
|
|
|
1234
1268
|
return this.#getPayload("integration_attempts", "task_id = ? AND integration_id = ?", [taskId, integrationId]);
|
|
1235
1269
|
}
|
|
1236
1270
|
// -- durable jobs -----------------------------------------------------------
|
|
1271
|
+
saveSchedule(schedule) {
|
|
1272
|
+
validateSchedule(schedule);
|
|
1273
|
+
this.#mutate(() => {
|
|
1274
|
+
this.#db.prepare(`INSERT INTO schedules VALUES(?,?,?,?,?)
|
|
1275
|
+
ON CONFLICT(scope_key,schedule_id) DO UPDATE SET payload=excluded.payload,state=excluded.state,next_due=excluded.next_due`)
|
|
1276
|
+
.run(jobScopeKey(schedule.scope), schedule.id, this.#recordJson("schedules", schedule), schedule.state, schedule.nextDue);
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
getSchedule(scope, id) {
|
|
1280
|
+
return this.#getPayload("schedules", "scope_key=? AND schedule_id=?", [jobScopeKey(scope), id]);
|
|
1281
|
+
}
|
|
1282
|
+
listSchedules(scope) {
|
|
1283
|
+
return this.#listPayload("schedules", "scope_key=?", [jobScopeKey(scope)]);
|
|
1284
|
+
}
|
|
1285
|
+
listDueSchedules(now, limit) {
|
|
1286
|
+
const rows = this.#db.prepare(`SELECT payload FROM schedules
|
|
1287
|
+
WHERE state='enabled' AND next_due<=?
|
|
1288
|
+
ORDER BY COALESCE(json_extract(payload,'$.lastAttempt.at'),''),next_due,scope_key,schedule_id LIMIT ?`).all(now, limit);
|
|
1289
|
+
return rows.map(row => validateSchedule(JSON.parse(row.payload)));
|
|
1290
|
+
}
|
|
1291
|
+
saveJobDefinition(job) {
|
|
1292
|
+
validateJobDefinition(job);
|
|
1293
|
+
if (job.scope.kind === "task")
|
|
1294
|
+
this.#requireTask(job.scope.taskId);
|
|
1295
|
+
const previous = this.getJobDefinition(job.scope, job.id);
|
|
1296
|
+
if (previous !== null && (job.revision !== previous.revision + 1
|
|
1297
|
+
|| job.createdAt !== previous.createdAt || Date.parse(job.updatedAt) < Date.parse(previous.updatedAt))) {
|
|
1298
|
+
throw new StorageRecordError("Job definition revision changed; reload before editing.");
|
|
1299
|
+
}
|
|
1300
|
+
this.#mutate(() => {
|
|
1301
|
+
this.#db.prepare(`INSERT INTO job_definitions VALUES(?,?,?)
|
|
1302
|
+
ON CONFLICT(scope_key,definition_id) DO UPDATE SET payload=excluded.payload`)
|
|
1303
|
+
.run(jobScopeKey(job.scope), job.id, this.#recordJson("job_definitions", job));
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
getJobDefinition(scope, id) {
|
|
1307
|
+
return this.#getPayload("job_definitions", "scope_key=? AND definition_id=?", [jobScopeKey(scope), id]);
|
|
1308
|
+
}
|
|
1309
|
+
listJobDefinitions(scope) {
|
|
1310
|
+
return this.#listPayload("job_definitions", "scope_key=?", [jobScopeKey(scope)]);
|
|
1311
|
+
}
|
|
1312
|
+
saveGlobalJobRun(run) {
|
|
1313
|
+
validateDurableJob(run);
|
|
1314
|
+
if ("taskId" in run || run.scope !== "global")
|
|
1315
|
+
throw new StorageRecordError("Global Run cannot have a Task.");
|
|
1316
|
+
const previous = this.getGlobalJobRun(run.id);
|
|
1317
|
+
if (previous !== null && (!validDurableJobTransition(previous, run)
|
|
1318
|
+
|| Date.parse(run.updatedAt) < Date.parse(previous.updatedAt))) {
|
|
1319
|
+
throw new StorageRecordError("Invalid global JobRun transition.");
|
|
1320
|
+
}
|
|
1321
|
+
this.#mutate(() => {
|
|
1322
|
+
this.#db.prepare(`INSERT INTO global_job_runs VALUES(?,?,?)
|
|
1323
|
+
ON CONFLICT(run_id) DO UPDATE SET payload=excluded.payload,status=excluded.status`)
|
|
1324
|
+
.run(run.id, this.#recordJson("global_job_runs", run), run.status);
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
getGlobalJobRun(id) {
|
|
1328
|
+
return this.#getPayload("global_job_runs", "run_id=?", [id]);
|
|
1329
|
+
}
|
|
1330
|
+
listGlobalJobRuns() {
|
|
1331
|
+
return this.#listPayload("global_job_runs", "1=1", []);
|
|
1332
|
+
}
|
|
1333
|
+
listActiveGlobalJobRuns() {
|
|
1334
|
+
return this.#listPayload("global_job_runs", "status IN ('queued','running')", []);
|
|
1335
|
+
}
|
|
1237
1336
|
nextDurableJobId(taskId) {
|
|
1238
1337
|
return this.#nextTaskRecordId(taskId, "durableJob");
|
|
1239
1338
|
}
|
|
@@ -1243,21 +1342,35 @@ export class SqliteTaskStore {
|
|
|
1243
1342
|
}
|
|
1244
1343
|
validateDurableJob(job);
|
|
1245
1344
|
this.#requireTask(taskId);
|
|
1246
|
-
const existing = this.getDurableJob(taskId, job.id);
|
|
1247
|
-
if (existing !== null) {
|
|
1248
|
-
if (Date.parse(job.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
1249
|
-
throw new StorageRecordError(`DurableJob updatedAt cannot move backwards: ${job.id}`);
|
|
1250
|
-
}
|
|
1251
|
-
if (!validDurableJobTransition(existing, job)) {
|
|
1252
|
-
throw new StorageRecordError(`DurableJob transition is invalid: ${job.id}`);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
1345
|
this.#mutate(() => {
|
|
1346
|
+
const existing = this.getDurableJob(taskId, job.id);
|
|
1347
|
+
if (existing !== null) {
|
|
1348
|
+
if (Date.parse(job.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
1349
|
+
throw new StorageRecordError(`DurableJob updatedAt cannot move backwards: ${job.id}`);
|
|
1350
|
+
}
|
|
1351
|
+
if (!validDurableJobTransition(existing, job)) {
|
|
1352
|
+
throw new StorageRecordError(`DurableJob transition is invalid: ${job.id}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1256
1355
|
this.#db.prepare(`INSERT INTO durable_jobs (job_id, task_id, idempotency_key, status, payload, created_at, updated_at)
|
|
1257
1356
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1258
1357
|
ON CONFLICT(task_id, job_id) DO UPDATE SET idempotency_key = excluded.idempotency_key,
|
|
1259
1358
|
status = excluded.status,
|
|
1260
1359
|
payload = excluded.payload, updated_at = excluded.updated_at`).run(job.id, taskId, job.idempotencyKey ?? null, job.status, this.#recordJson("durable_jobs", job), job.createdAt, job.updatedAt);
|
|
1360
|
+
// All Task Job writers share this boundary (including cancellation and
|
|
1361
|
+
// unknown-result acknowledgement). Publish semantic changes atomically;
|
|
1362
|
+
// heartbeat/checkpoint/notification bookkeeping is deliberately excluded.
|
|
1363
|
+
if (existing === null || existing.status !== job.status
|
|
1364
|
+
|| !isDeepStrictEqual(existing.result, job.result)
|
|
1365
|
+
|| existing.cancelRequestedAt !== job.cancelRequestedAt
|
|
1366
|
+
|| existing.acknowledgedAt !== job.acknowledgedAt) {
|
|
1367
|
+
this.saveEvent(taskId, createTaskEvent(this.nextEventId(taskId), taskId, "job.state-changed", {
|
|
1368
|
+
jobId: job.id, status: job.status,
|
|
1369
|
+
...(existing === null ? {} : { previousStatus: existing.status }),
|
|
1370
|
+
...(job.cancelRequestedAt === undefined ? {} : { cancelRequestedAt: job.cancelRequestedAt }),
|
|
1371
|
+
...(job.acknowledgedAt === undefined ? {} : { acknowledgedAt: job.acknowledgedAt })
|
|
1372
|
+
}, new Date(job.updatedAt)));
|
|
1373
|
+
}
|
|
1261
1374
|
});
|
|
1262
1375
|
}
|
|
1263
1376
|
listDurableJobs(taskId) {
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -70,6 +70,11 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
70
70
|
"listIntegrationAttempts",
|
|
71
71
|
"getIntegrationAttempt",
|
|
72
72
|
"listActiveDurableJobs",
|
|
73
|
+
"getJobDefinition",
|
|
74
|
+
"listJobDefinitions",
|
|
75
|
+
"getGlobalJobRun",
|
|
76
|
+
"listGlobalJobRuns",
|
|
77
|
+
"listActiveGlobalJobRuns",
|
|
73
78
|
"listRoles",
|
|
74
79
|
"getRole",
|
|
75
80
|
"listManagedWorkspaces",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { validateDurableJob } from "../../job/durableJob.js";
|
|
2
|
+
import { definitionFromTaskRun } from "../../job/jobDefinition.js";
|
|
3
|
+
export function migrateJobDefinitions(db) {
|
|
4
|
+
const rows = db.prepare("SELECT task_id, job_id, payload FROM durable_jobs").all();
|
|
5
|
+
for (const row of rows) {
|
|
6
|
+
const original = JSON.parse(row.payload);
|
|
7
|
+
if (original.taskId !== row.task_id || original.id !== row.job_id
|
|
8
|
+
|| original.definitionId !== undefined || original.definitionRevision !== undefined || original.trigger !== undefined) {
|
|
9
|
+
throw new Error(`Invalid 1.8 Job identity: ${row.task_id}/${row.job_id}.`);
|
|
10
|
+
}
|
|
11
|
+
const run = validateDurableJob({
|
|
12
|
+
...original, definitionId: original.id, definitionRevision: 1,
|
|
13
|
+
trigger: { source: "migration", key: original.operation.requestId }
|
|
14
|
+
});
|
|
15
|
+
db.prepare("INSERT INTO storage_migration_archive(migration_version,family,record_key,payload) VALUES(?,?,?,?)")
|
|
16
|
+
.run(9, "durable_jobs", `${row.task_id}/${row.job_id}`, row.payload);
|
|
17
|
+
db.prepare("INSERT INTO job_definitions VALUES(?,?,?)")
|
|
18
|
+
.run(row.task_id, run.definitionId, JSON.stringify(definitionFromTaskRun(run)));
|
|
19
|
+
db.prepare("UPDATE durable_jobs SET payload=? WHERE task_id=? AND job_id=?")
|
|
20
|
+
.run(JSON.stringify(run), row.task_id, row.job_id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -76,7 +76,7 @@ export function archiveRetainedResources(store, task) {
|
|
|
76
76
|
return resources;
|
|
77
77
|
}
|
|
78
78
|
export function archiveDeliveryWarnings(delivery) {
|
|
79
|
-
return delivery.projects.filter(p => p.
|
|
79
|
+
return delivery.projects.filter(p => p.deliveryRequired && (!p.merged || !p.verified))
|
|
80
80
|
.map(p => ({ resource: `delivery:${delivery.taskId}/${p.projectId}`,
|
|
81
81
|
detail: `${p.coverage}: ${p.reason} Merged=${p.merged}; verified=${p.verified}.` }));
|
|
82
82
|
}
|
|
@@ -65,7 +65,7 @@ export async function inspectTaskArchive(coordinator, request) {
|
|
|
65
65
|
throw new Error(`Task not found: ${request.taskId}.`);
|
|
66
66
|
const settlement = archiveSettlementChecks(store, task);
|
|
67
67
|
const delivery = await readArchiveDelivery(store, preparer.git, task, request.force);
|
|
68
|
-
const deliveryChecks = delivery.projects.filter(p => p.
|
|
68
|
+
const deliveryChecks = delivery.projects.filter(p => p.deliveryRequired && (!p.merged || !p.verified))
|
|
69
69
|
.map(p => ({ resource: `delivery:${task.id}/${p.projectId}`, reason: "delivery-coverage", status: "blocked",
|
|
70
70
|
detail: p.reason, expected: { merged: true, verified: true, localCommit: p.expectedLocalCommit },
|
|
71
71
|
observed: { coverage: p.coverage, merged: p.merged, verified: p.verified, localCommit: p.publication?.localCommit ?? null },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** An append-only Task decision, not a Project permission or a merge fact. */
|
|
2
|
+
export const PROJECT_DELIVERY_DECIDED_EVENT = "task.project-delivery-decided";
|
|
3
|
+
export function projectDeliveryDecision(events, taskId, projectId) {
|
|
4
|
+
return events.filter(event => event.taskId === taskId
|
|
5
|
+
&& event.type === PROJECT_DELIVERY_DECIDED_EVENT && event.payload.projectId === projectId)
|
|
6
|
+
// Task event IDs are monotonic durable sequence numbers; wall clocks can move backwards.
|
|
7
|
+
.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true })).at(-1) ?? null;
|
|
8
|
+
}
|
|
9
|
+
export function projectDeliveryRequirement(decision) {
|
|
10
|
+
if (decision === null)
|
|
11
|
+
return "required";
|
|
12
|
+
const p = decision.payload;
|
|
13
|
+
if (!["required", "excluded"].includes(p.requirement ?? "")
|
|
14
|
+
|| !p.reason?.trim() || !p.sourceMessageId?.trim()
|
|
15
|
+
|| !/^[a-f0-9]{64}$/u.test(p.sourceDigest ?? "") || !p.purpose?.trim()
|
|
16
|
+
|| !["user", "operator", "leader"].includes(p.by ?? "")) {
|
|
17
|
+
throw new Error(`Project delivery decision ${decision.id} has incomplete evidence; preserve it for diagnosis.`);
|
|
18
|
+
}
|
|
19
|
+
return p.requirement;
|
|
20
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
const REQUIRED = [
|
|
3
|
+
"taskId", "publicationId", "projectId", "localCommit", "provider", "repository",
|
|
4
|
+
"externalKind", "externalId", "externalUrl", "targetBranch", "state",
|
|
5
|
+
"headCommit", "observedAt", "tool", "command", "output"
|
|
6
|
+
];
|
|
7
|
+
const OPTIONAL = ["remoteCommit", "mergedAt"];
|
|
8
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
9
|
+
const MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
10
|
+
export function parsePublicationToolObservation(text) {
|
|
11
|
+
if (Buffer.byteLength(text, "utf8") > 64 * 1024) {
|
|
12
|
+
throw usageError("Tool observation must not exceed 64 KiB; supply a sanitized relevant output excerpt.");
|
|
13
|
+
}
|
|
14
|
+
let value;
|
|
15
|
+
try {
|
|
16
|
+
value = JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw usageError("--observation requires a JSON object.");
|
|
20
|
+
}
|
|
21
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
+
throw usageError("--observation requires a JSON object.");
|
|
23
|
+
}
|
|
24
|
+
const record = value;
|
|
25
|
+
const allowed = new Set([...REQUIRED, ...OPTIONAL]);
|
|
26
|
+
for (const key of Object.keys(record)) {
|
|
27
|
+
if (!allowed.has(key))
|
|
28
|
+
throw usageError(`Unknown tool observation field: ${key}.`);
|
|
29
|
+
}
|
|
30
|
+
for (const key of [...REQUIRED, ...OPTIONAL]) {
|
|
31
|
+
if (OPTIONAL.includes(key) && record[key] === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
if (typeof record[key] !== "string" || record[key].trim().length === 0) {
|
|
34
|
+
throw usageError(`Tool observation requires nonempty ${key}.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const key of ["localCommit", "headCommit", "remoteCommit"]) {
|
|
38
|
+
if (record[key] !== undefined && !SHA.test(record[key])) {
|
|
39
|
+
throw usageError(`Tool observation ${key} requires an exact lowercase Git SHA.`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!((record.provider === "github" && record.externalKind === "pull-request")
|
|
43
|
+
|| (record.provider === "gitlab" && record.externalKind === "merge-request"))) {
|
|
44
|
+
throw usageError("Tool observation requires a matching GitHub PR or GitLab MR kind.");
|
|
45
|
+
}
|
|
46
|
+
if (!["open", "merged", "closed"].includes(record.state)
|
|
47
|
+
|| (record.state === "merged" && record.remoteCommit === undefined)) {
|
|
48
|
+
throw usageError("Tool observation requires a known state and an integrated commit for a merge.");
|
|
49
|
+
}
|
|
50
|
+
for (const key of ["observedAt", "mergedAt"]) {
|
|
51
|
+
if (record[key] !== undefined
|
|
52
|
+
&& (!/^\d{4}-\d\d-\d\dT/u.test(record[key])
|
|
53
|
+
|| !Number.isFinite(Date.parse(record[key])))) {
|
|
54
|
+
throw usageError(`Tool observation ${key} requires an ISO timestamp.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Canonical key ordering makes retries independent of JSON whitespace/order.
|
|
58
|
+
const evidence = JSON.stringify(Object.fromEntries(Object.keys(record).sort().map(key => [key, record[key]])));
|
|
59
|
+
return { ...record, evidence };
|
|
60
|
+
}
|
|
61
|
+
export function assertPublicationToolObservation(publication, observation, now, observationNotBefore) {
|
|
62
|
+
for (const field of ["taskId", "projectId", "provider", "repository",
|
|
63
|
+
"externalKind", "externalId", "localCommit", "targetBranch"]) {
|
|
64
|
+
if (observation[field] !== publication[field]) {
|
|
65
|
+
throw usageError(`Tool observation ${field} does not match the Publication candidate.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (observation.publicationId !== publication.id) {
|
|
69
|
+
throw usageError("Tool observation publicationId does not match the selected Publication.");
|
|
70
|
+
}
|
|
71
|
+
if (publication.externalUrl === undefined
|
|
72
|
+
|| canonicalPublicationUrl(observation.externalUrl, publication.provider)
|
|
73
|
+
!== canonicalPublicationUrl(publication.externalUrl, publication.provider)) {
|
|
74
|
+
throw usageError("Tool observation externalUrl must match the recorded Publication service and object.");
|
|
75
|
+
}
|
|
76
|
+
const url = new URL(canonicalPublicationUrl(observation.externalUrl, publication.provider));
|
|
77
|
+
// GitLab UI routes are deployment-specific. Structured identity above and
|
|
78
|
+
// the complete registered URL bind the object without inventing a web route.
|
|
79
|
+
if (publication.provider === "github"
|
|
80
|
+
&& url.pathname !== `/${publication.repository.toLowerCase()}/pull/${publication.externalId}`) {
|
|
81
|
+
throw usageError("Tool observation URL does not match the repository and PR/MR identity.");
|
|
82
|
+
}
|
|
83
|
+
const observed = Date.parse(observation.observedAt);
|
|
84
|
+
if ((observationNotBefore !== undefined && observed < observationNotBefore)
|
|
85
|
+
|| observed > now.getTime() || now.getTime() - observed > MAX_AGE_MS) {
|
|
86
|
+
throw usageError("Tool observation is stale or future-dated; observe the current Publication within 24 hours of recording.");
|
|
87
|
+
}
|
|
88
|
+
if (observation.mergedAt !== undefined && Date.parse(observation.mergedAt) > observed) {
|
|
89
|
+
throw usageError("Tool observation merge time cannot follow its observation time.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Derive the latest material change from immutable history, not title edits or
|
|
93
|
+
* initial bookkeeping. A tool read may legitimately precede its first upsert.
|
|
94
|
+
* Changed-then-restored candidates still have a change boundary. */
|
|
95
|
+
export function publicationObservationNotBefore(publication, previous) {
|
|
96
|
+
const fields = [
|
|
97
|
+
"projectId", "provider", "repository", "externalKind", "externalId",
|
|
98
|
+
"externalUrl", "localCommit", "targetBranch", "state", "headCommit", "remoteCommit", "mergedAt"
|
|
99
|
+
];
|
|
100
|
+
let current = publication;
|
|
101
|
+
while (current.supersedes !== undefined) {
|
|
102
|
+
const prior = previous(current.supersedes);
|
|
103
|
+
if (prior === null)
|
|
104
|
+
throw usageError("Publication history is incomplete; cannot validate observation freshness.");
|
|
105
|
+
if (fields.some(field => current[field] !== prior[field])) {
|
|
106
|
+
return Date.parse(current.createdAt);
|
|
107
|
+
}
|
|
108
|
+
current = prior;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
export function canonicalPublicationUrl(value, provider) {
|
|
113
|
+
let url;
|
|
114
|
+
try {
|
|
115
|
+
url = new URL(value);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw usageError("Publication URL must be an absolute HTTP(S) URL.");
|
|
119
|
+
}
|
|
120
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password
|
|
121
|
+
|| url.search || url.hash) {
|
|
122
|
+
throw usageError("Publication URL must be HTTP(S), without credentials, query or fragment.");
|
|
123
|
+
}
|
|
124
|
+
const path = url.pathname.replace(/\/$/u, "");
|
|
125
|
+
return `${url.origin}${provider === "github" ? path.toLowerCase() : path}`;
|
|
126
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { publicationAdoption } from "./publicationAdoption.js";
|
|
2
|
+
import { projectDeliveryDecision, projectDeliveryRequirement } from "./projectDeliveryScope.js";
|
|
2
3
|
/**
|
|
3
4
|
* One read-only delivery projection over existing Task, workspace, completion,
|
|
4
5
|
* and Publication facts. It never writes a merged flag or queries a provider.
|
|
@@ -10,6 +11,8 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
10
11
|
const currentPublications = currentPublicationReferences(facts.publications);
|
|
11
12
|
const projects = facts.task.projectBindings.map((binding) => {
|
|
12
13
|
const expectedLocalCommit = expected.commits.get(binding.projectId) ?? null;
|
|
14
|
+
const scopeDecision = projectDeliveryDecision(facts.events, facts.task.id, binding.projectId);
|
|
15
|
+
const deliveryRequirement = projectDeliveryRequirement(scopeDecision);
|
|
13
16
|
const baseCommit = baseCommits.get(binding.projectId) ?? null;
|
|
14
17
|
const codeDelivery = expectedLocalCommit === null
|
|
15
18
|
? "unknown"
|
|
@@ -44,6 +47,9 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
44
47
|
deliveryLocalCommit: covers ? publication.localCommit ?? null : null,
|
|
45
48
|
adoption,
|
|
46
49
|
codeDelivery,
|
|
50
|
+
deliveryRequirement,
|
|
51
|
+
scopeDecision,
|
|
52
|
+
deliveryRequired: deliveryRequirement === "required" && codeDelivery !== "none",
|
|
47
53
|
publication,
|
|
48
54
|
state: publication?.state ?? null,
|
|
49
55
|
verification: publication?.verification ?? null,
|
|
@@ -51,23 +57,26 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
51
57
|
coverage,
|
|
52
58
|
merged,
|
|
53
59
|
verified,
|
|
54
|
-
reason:
|
|
60
|
+
reason: (deliveryRequirement === "excluded"
|
|
61
|
+
? `No delivery obligation: ${scopeDecision.payload.reason} (${scopeDecision.id}). Merge and cleanup facts remain separate. `
|
|
62
|
+
: "") + projectCoverageReason(expectedLocalCommit, publication, coverage, adoption) + (coverage === "stale" && adoptionEvidence?.reason
|
|
55
63
|
? ` ${adoptionEvidence.reason}` : "")
|
|
56
64
|
};
|
|
57
65
|
});
|
|
58
66
|
const codeProjects = projects.filter((project) => project.codeDelivery !== "none");
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
67
|
+
const requiredProjects = projects.filter(project => project.deliveryRequired);
|
|
68
|
+
const mergedProjectCount = requiredProjects.filter((project) => project.merged).length;
|
|
69
|
+
const verifiedProjectCount = requiredProjects.filter((project) => project.verified).length;
|
|
70
|
+
const allMerged = requiredProjects.every((project) => project.merged);
|
|
71
|
+
const allVerified = requiredProjects.every((project) => project.verified);
|
|
72
|
+
const status = requiredProjects.length === 0
|
|
64
73
|
? "none"
|
|
65
|
-
:
|
|
74
|
+
: requiredProjects.some((project) => project.coverage === "head-unavailable")
|
|
66
75
|
? "unavailable"
|
|
67
76
|
: allMerged
|
|
68
77
|
? allVerified ? "merged" : "unverified"
|
|
69
78
|
: mergedProjectCount > 0 ? "partial"
|
|
70
|
-
:
|
|
79
|
+
: requiredProjects.some(p => p.state === "merged") ? "uncovered" : "pending";
|
|
71
80
|
return {
|
|
72
81
|
taskId: facts.task.id,
|
|
73
82
|
source: expected.source,
|
|
@@ -81,6 +90,8 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
81
90
|
allVerified,
|
|
82
91
|
totalProjectCount: projects.length,
|
|
83
92
|
codeProjectCount: codeProjects.length,
|
|
93
|
+
requiredCodeProjectCount: requiredProjects.length,
|
|
94
|
+
excludedProjectCount: projects.filter(project => project.deliveryRequirement === "excluded").length,
|
|
84
95
|
mergedProjectCount,
|
|
85
96
|
verifiedProjectCount,
|
|
86
97
|
integratedCoverageSatisfied: allMerged && allVerified,
|
|
@@ -2,6 +2,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { usageError } from "../errors/cliError.js";
|
|
3
3
|
import { projectTaskRemoteDelivery } from "../task/remoteDelivery.js";
|
|
4
4
|
import { PUBLICATION_ADOPTED_EVENT } from "./publicationAdoption.js";
|
|
5
|
+
import { PROJECT_DELIVERY_DECIDED_EVENT } from "./projectDeliveryScope.js";
|
|
5
6
|
export function projectTaskRemoteDeliveryFromStore(store, task, currentCandidate = null) {
|
|
6
7
|
return createTaskRemoteDeliveryProof(store, task, currentCandidate).delivery;
|
|
7
8
|
}
|
|
@@ -10,7 +11,7 @@ export function createTaskRemoteDeliveryProof(store, task, currentCandidate = nu
|
|
|
10
11
|
const delivery = projectTaskRemoteDelivery({
|
|
11
12
|
task,
|
|
12
13
|
events: store.listEventsByType(task.id, [
|
|
13
|
-
"task.completed", "task.archived", PUBLICATION_ADOPTED_EVENT
|
|
14
|
+
"task.completed", "task.archived", PUBLICATION_ADOPTED_EVENT, PROJECT_DELIVERY_DECIDED_EVENT
|
|
14
15
|
]),
|
|
15
16
|
publications,
|
|
16
17
|
managedWorkspaces: store.listManagedWorkspaces(task.id),
|
|
@@ -49,13 +50,13 @@ export function assertTaskRemoteDeliveryProof(store, task, proof) {
|
|
|
49
50
|
}
|
|
50
51
|
export function assertTaskRemoteDeliveryIntegrated(delivery) {
|
|
51
52
|
if (!delivery.allMerged) {
|
|
52
|
-
const headUnavailable = delivery.projects.filter((project) => (project.
|
|
53
|
+
const headUnavailable = delivery.projects.filter((project) => (project.deliveryRequired
|
|
53
54
|
&& project.coverage === "head-unavailable"));
|
|
54
55
|
if (headUnavailable.length > 0 && delivery.source === "task-completed") {
|
|
55
56
|
throw usageError(`Task ${delivery.taskId} has no frozen completion heads for: ${headUnavailable.map(({ projectId }) => projectId).join(", ")}. Historical acceptance cannot be inferred from a merge or archive.`);
|
|
56
57
|
}
|
|
57
58
|
const uncovered = delivery.projects
|
|
58
|
-
.filter((project) => project.
|
|
59
|
+
.filter((project) => project.deliveryRequired && !project.merged)
|
|
59
60
|
.map((project) => (`${project.projectId}/${project.coverage}`
|
|
60
61
|
+ (project.publication === null ? "" : `/${project.publication.id}`)))
|
|
61
62
|
.join(", ");
|
|
@@ -65,7 +66,7 @@ export function assertTaskRemoteDeliveryIntegrated(delivery) {
|
|
|
65
66
|
if (delivery.allVerified)
|
|
66
67
|
return;
|
|
67
68
|
const unverified = delivery.projects
|
|
68
|
-
.filter((project) => (project.
|
|
69
|
+
.filter((project) => (project.deliveryRequired
|
|
69
70
|
&& project.merged
|
|
70
71
|
&& !project.verified))
|
|
71
72
|
.map((project) => (`${project.projectId}/${project.coverage}`
|
|
@@ -3,6 +3,8 @@ import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
|
3
3
|
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
4
4
|
import { admitTaskActivationRequest, adoptTaskActivationRequest, cancelTaskActivationRequest, canonicalEnvironmentPlan, createTaskActivationRequest, describeEnvironmentPlan, failTaskActivationRequest, settledActivationFromEvents, taskActivationInputDigest, TASK_ACTIVATION_EVENT } from "./taskActivation.js";
|
|
5
5
|
import { setTaskActivationRequest, recordTaskActivationRequestEvidence } from "./task.js";
|
|
6
|
+
import { contextContentDigest } from "../context/contextSnapshot.js";
|
|
7
|
+
import { activationSourceEvent } from "../context/taskPhase.js";
|
|
6
8
|
/**
|
|
7
9
|
* The terminal outcome already recorded for a requestId, or `undefined` if it
|
|
8
10
|
* was never cancelled or adopted.
|
|
@@ -63,6 +65,20 @@ export function recordTaskActivationRequestInTransaction(tx, input, now) {
|
|
|
63
65
|
? "immediate"
|
|
64
66
|
: "after-planning-turn";
|
|
65
67
|
const existing = task.activationRequest;
|
|
68
|
+
if (input.sourceMessageId !== undefined && input.sourceText !== undefined) {
|
|
69
|
+
throw new Error("Activation source requires either a Message or exact text, not both.");
|
|
70
|
+
}
|
|
71
|
+
if (input.sourceText !== undefined && (!input.sourceText.trim() || input.sourceText.includes("\0"))) {
|
|
72
|
+
throw new Error("Activation source text must be nonempty exact input.");
|
|
73
|
+
}
|
|
74
|
+
const sourceMessage = input.sourceMessageId === undefined ? undefined : tx.queryContextRecords(task.id, {
|
|
75
|
+
family: "task-message", ids: [input.sourceMessageId], limit: 1
|
|
76
|
+
}).records[0]?.value;
|
|
77
|
+
if (input.sourceMessageId !== undefined && (!sourceMessage
|
|
78
|
+
|| !["user", "operator"].includes(sourceMessage.kind) || sourceMessage.recipient !== undefined
|
|
79
|
+
|| sourceMessage.workItemId !== undefined)) {
|
|
80
|
+
throw new Error("Activation source must reference an existing shared user/Operator Message in this Task.");
|
|
81
|
+
}
|
|
66
82
|
// A requestId whose outcome was already decided is never re-opened, whether
|
|
67
83
|
// it still holds the slot or a later request displaced it. Cancelling A,
|
|
68
84
|
// requesting B and then retrying A must not resurrect A: the withdrawal is
|
|
@@ -81,6 +97,11 @@ export function recordTaskActivationRequestInTransaction(tx, input, now) {
|
|
|
81
97
|
+ "Use a new requestId.");
|
|
82
98
|
}
|
|
83
99
|
if (existing?.operation.requestId === input.requestId) {
|
|
100
|
+
const source = activationSourceEvent(tx, task)?.payload;
|
|
101
|
+
if (source?.sourceMessageId !== input.sourceMessageId || source?.sourceText !== input.sourceText
|
|
102
|
+
|| (sourceMessage !== undefined && source?.sourceBodyDigest !== contextContentDigest(sourceMessage.body))) {
|
|
103
|
+
throw new Error(`Activation requestId ${input.requestId} was already used with a different source.`);
|
|
104
|
+
}
|
|
84
105
|
const digest = taskActivationInputDigest(task.id, {
|
|
85
106
|
startMode,
|
|
86
107
|
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
|
|
@@ -112,11 +133,25 @@ export function recordTaskActivationRequestInTransaction(tx, input, now) {
|
|
|
112
133
|
environmentPlan: plan
|
|
113
134
|
}, now);
|
|
114
135
|
tx.saveTask(setTaskActivationRequest(task, request, now));
|
|
136
|
+
const priorMessageId = tx.queryContextRecords(task.id, {
|
|
137
|
+
family: "task-message", descending: true, limit: 1
|
|
138
|
+
}).records[0]?.id;
|
|
115
139
|
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, TASK_ACTIVATION_EVENT.requested, {
|
|
116
140
|
requestId: request.operation.requestId,
|
|
117
141
|
startMode: request.startMode,
|
|
118
142
|
environmentPlan: describeEnvironmentPlan(request.environmentPlan),
|
|
119
143
|
actor: request.operation.actorId,
|
|
144
|
+
...(priorMessageId === undefined ? {} : { priorMessageId }),
|
|
145
|
+
...(input.sourceMessageId === undefined ? {} : {
|
|
146
|
+
sourceMessageId: input.sourceMessageId,
|
|
147
|
+
sourceBodyDigest: contextContentDigest(sourceMessage.body),
|
|
148
|
+
sourceKind: "task-message"
|
|
149
|
+
}),
|
|
150
|
+
...(input.sourceText === undefined ? {} : {
|
|
151
|
+
sourceText: input.sourceText,
|
|
152
|
+
sourceKind: input.actorId.startsWith("task:") ? "leader-attested-native-input" : "caller-provided-input",
|
|
153
|
+
...(input.sourceNativeSessionId === undefined ? {} : { sourceNativeSessionId: input.sourceNativeSessionId })
|
|
154
|
+
}),
|
|
120
155
|
...(request.afterPlanningRun === undefined
|
|
121
156
|
? {}
|
|
122
157
|
: { afterPlanningRun: request.afterPlanningRun })
|
|
@@ -8,6 +8,7 @@ import { createThemeController } from "/assets/js/lib/theme.js";
|
|
|
8
8
|
import { clearPreference, readSessionAccessMode, writeSessionAccessMode } from "/assets/js/lib/prefs.js";
|
|
9
9
|
import { i18n, createManageShell, pageSections, registerManagePage, navigateManage } from "/assets/js/layout/manageShell.js";
|
|
10
10
|
import { mountSkills } from "/assets/js/views/skills.js";
|
|
11
|
+
import { mountJobs } from "/assets/js/views/jobs.js";
|
|
11
12
|
|
|
12
13
|
const $ = selector => document.querySelector(selector);
|
|
13
14
|
const t = i18n.t;
|
|
@@ -18,11 +19,13 @@ function readSection() {
|
|
|
18
19
|
return SECTIONS.includes(section) ? section : SECTIONS[0];
|
|
19
20
|
}
|
|
20
21
|
let section = readSection();
|
|
22
|
+
let jobs;
|
|
21
23
|
const shell = createManageShell({ page: "settings", t, onNavigate: navigate });
|
|
22
24
|
function showSection(next) {
|
|
23
25
|
section = next;
|
|
24
26
|
document.querySelectorAll("#settings-root > .manage-section[data-section]").forEach(panel => { panel.hidden = panel.dataset.section !== next; });
|
|
25
27
|
shell.set(next);
|
|
28
|
+
if (next === "jobs" && !jobs) jobs = mountJobs($("#global-jobs"), t);
|
|
26
29
|
}
|
|
27
30
|
function navigate(next) {
|
|
28
31
|
navigateManage({ page: "settings", section: next });
|
|
@@ -34,6 +37,9 @@ function markDirty(name, value) {
|
|
|
34
37
|
}
|
|
35
38
|
registerManagePage("settings", { show: next => showSection(next.section) });
|
|
36
39
|
i18n.subscribe(() => shell.render());
|
|
40
|
+
i18n.subscribe(() => {
|
|
41
|
+
if (jobs && !jobs.isDirty() && !jobs.isBusy()) jobs = mountJobs($("#global-jobs"), t);
|
|
42
|
+
});
|
|
37
43
|
showSection(section);
|
|
38
44
|
const skills = mountSkills($("#skill-browser"), t, { heading: false, onDirty: value => markDirty("skills", value) });
|
|
39
45
|
i18n.subscribe(() => skills?.translate());
|
|
@@ -59,7 +65,7 @@ let searchTimer;
|
|
|
59
65
|
const more = h("button.btn", { type: "button", hidden: true }, t("settings.more"));
|
|
60
66
|
container.after(more);
|
|
61
67
|
window.addEventListener("beforeunload", event => {
|
|
62
|
-
if (!groups.some(g => g.dirty)) return;
|
|
68
|
+
if (!groups.some(g => g.dirty) && !jobs?.isDirty()) return;
|
|
63
69
|
event.preventDefault(); event.returnValue = "";
|
|
64
70
|
});
|
|
65
71
|
|