@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.
Files changed (119) hide show
  1. package/README.md +5 -3
  2. package/dist/cli/commandCatalog.js +56 -21
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli/interactionPolicy.js +2 -2
  5. package/dist/commands/durableJobCommands.js +50 -0
  6. package/dist/commands/globalRoleCommands.js +30 -9
  7. package/dist/commands/inboxCommands.js +79 -22
  8. package/dist/commands/projectCommands.js +4 -33
  9. package/dist/commands/scheduleCommands.js +45 -0
  10. package/dist/commands/taskActivationCommands.js +19 -5
  11. package/dist/commands/taskCommands.js +8 -2
  12. package/dist/commands/taskFactCommands.js +24 -12
  13. package/dist/commands/taskProjectDeliveryCommand.js +56 -0
  14. package/dist/commands/taskPublicationCommands.js +6 -3
  15. package/dist/commands/taskPublicationVerifyCommand.js +74 -28
  16. package/dist/commands/taskRemoteDeliveryCommand.js +5 -3
  17. package/dist/context/runContextPack.js +25 -4
  18. package/dist/context/runInputContract.js +1 -0
  19. package/dist/context/taskContext.js +53 -16
  20. package/dist/context/taskPhase.js +99 -0
  21. package/dist/controlPlaneCli.js +38 -17
  22. package/dist/controller/controller.js +35 -0
  23. package/dist/controller/fileSchedulerStoreAdapter.js +75 -16
  24. package/dist/controller/globalInputDelivery.js +17 -7
  25. package/dist/controller/inboxTurnCheck.js +64 -0
  26. package/dist/controller/jobControl.js +42 -12
  27. package/dist/controller/jobDefinitionControl.js +304 -0
  28. package/dist/controller/jobSupervisor.js +47 -32
  29. package/dist/controller/providerRetryAdmission.js +2 -0
  30. package/dist/controller/runtime.js +11 -2
  31. package/dist/controller/scheduleControl.js +176 -0
  32. package/dist/coordination/workMailbox.js +3 -3
  33. package/dist/external/githubPublicationVerifier.js +14 -5
  34. package/dist/external/gitlabPublicationVerifier.js +12 -29
  35. package/dist/grant/taskAuthorization.js +7 -2
  36. package/dist/interaction/notificationPresentation.js +12 -3
  37. package/dist/interaction/operatorPresentation.js +5 -0
  38. package/dist/job/builtinJob.js +58 -0
  39. package/dist/job/durableJob.js +72 -49
  40. package/dist/job/jobDefinition.js +68 -0
  41. package/dist/job/jobRunner.js +2 -1
  42. package/dist/job/projectRefreshRunner.js +48 -0
  43. package/dist/message/globalInboxRecovery.js +44 -0
  44. package/dist/message/globalInterrupt.js +1 -1
  45. package/dist/message/inboxReceipt.js +14 -6
  46. package/dist/message/inboxTurn.js +86 -0
  47. package/dist/message/message.js +11 -0
  48. package/dist/message/taskInboxOwnership.js +20 -6
  49. package/dist/repository/projectRefresh.js +58 -0
  50. package/dist/repository/taskWorkspacePreparer.js +2 -1
  51. package/dist/repository/workspaceCleanupInspection.js +6 -1
  52. package/dist/schedule/schedule.js +108 -0
  53. package/dist/scheduler/operatorEvent.js +2 -0
  54. package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
  55. package/dist/storage/contextRecords.js +8 -0
  56. package/dist/storage/jobSchema.js +15 -0
  57. package/dist/storage/maintenanceCaller.js +1 -1
  58. package/dist/storage/recordValidation.js +5 -0
  59. package/dist/storage/scheduleSchema.js +12 -0
  60. package/dist/storage/sqliteSchema.js +95 -24
  61. package/dist/storage/sqliteStore.js +123 -10
  62. package/dist/storage/storageVersions.js +1 -1
  63. package/dist/storage/storeRpc.js +5 -0
  64. package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
  65. package/dist/task/archiveDiagnostics.js +1 -1
  66. package/dist/task/archivePreflight.js +1 -1
  67. package/dist/task/projectDeliveryScope.js +20 -0
  68. package/dist/task/publicationToolObservation.js +126 -0
  69. package/dist/task/remoteDelivery.js +19 -8
  70. package/dist/task/remoteDeliveryService.js +5 -4
  71. package/dist/task/taskActivationService.js +35 -0
  72. package/dist/web/assets/client/app/settings.js +7 -1
  73. package/dist/web/assets/client/i18n/en.js +65 -2
  74. package/dist/web/assets/client/i18n/zh-CN.js +65 -2
  75. package/dist/web/assets/client/index.js +2 -0
  76. package/dist/web/assets/client/views/jobs.js +222 -0
  77. package/dist/web/assets/client/views/task/deliveryResults.js +6 -2
  78. package/dist/web/assets/client/views/task/page.js +11 -1
  79. package/dist/web/assets/shared/manageNav.js +1 -0
  80. package/dist/web/assets/shared/taskTabs.js +2 -2
  81. package/dist/web/assets/shell/settingsContent.js +3 -0
  82. package/dist/web/assets/styles/index.js +2 -0
  83. package/dist/web/assets/styles/views/jobs.js +19 -0
  84. package/dist/web/controllerWeb.js +73 -21
  85. package/dist/web/taskPreviews.js +21 -13
  86. package/dist/web/webAccess.js +109 -0
  87. package/dist/web/webJobs.js +52 -0
  88. package/dist/web/webServer.js +76 -44
  89. package/docs/architecture/capabilities-and-resources.md +6 -4
  90. package/docs/architecture/capabilities-and-resources.zh-CN.md +5 -4
  91. package/docs/cli-information-contract.md +57 -4
  92. package/docs/cli-information-contract.zh-CN.md +41 -2
  93. package/docs/job-runs.md +224 -0
  94. package/docs/project-delivery-scope.md +59 -0
  95. package/docs/release-workflow.md +86 -0
  96. package/docs/release-workflow.zh-CN.md +68 -0
  97. package/docs/schedules.md +132 -0
  98. package/docs/storage-baseline.md +31 -1
  99. package/docs/storage-baseline.zh-CN.md +25 -1
  100. package/docs/task-delivery.md +74 -2
  101. package/docs/task-delivery.zh-CN.md +39 -1
  102. package/docs/testing/verification-levels.md +6 -2
  103. package/docs/testing/verification-levels.zh-CN.md +6 -3
  104. package/i18n/README.zh-CN.md +6 -3
  105. package/package.json +4 -3
  106. package/skills/yui-leader/SKILL.md +44 -1
  107. package/skills/yui-leader/VERSION +1 -1
  108. package/skills/yui-leader/references/planning.md +9 -1
  109. package/skills/yui-operator/SKILL.md +31 -0
  110. package/skills/yui-operator/VERSION +1 -1
  111. package/skills/yui-reviewer/SKILL.md +4 -0
  112. package/skills/yui-reviewer/VERSION +1 -1
  113. package/skills/yui-runtime/SKILL.md +67 -8
  114. package/skills/yui-runtime/VERSION +1 -1
  115. package/skills/yui-runtime/references/context-ordering.md +103 -0
  116. package/skills/yui-runtime/references/publication.md +44 -4
  117. package/skills/yui-runtime/references/recovery.md +21 -0
  118. package/skills/yui-worker/SKILL.md +5 -0
  119. package/skills/yui-worker/VERSION +1 -1
@@ -3,6 +3,67 @@
3
3
  // domain vocabulary (statuses, owners, actions) keeps the exact product terms.
4
4
  // Placeholders such as {count} are filled by fill() in lib/format.
5
5
  export const EN = {
6
+ "tabs.jobs": "Jobs",
7
+ "settings.section.jobs": "Jobs & schedules",
8
+ "jobs.heading": "Jobs & schedules",
9
+ "jobs.help": "Run now starts one execution without changing the cadence. Cancel stops only the selected Run. Disable prevents future scheduled admissions. Operations use the current Operator binding.",
10
+ "jobs.details": "Details",
11
+ "jobs.scope": "Ownership",
12
+ "jobs.project": "Target Project",
13
+ "jobs.name": "Title",
14
+ "jobs.action": "Controlled action",
15
+ "jobs.newJob": "Create Job",
16
+ "jobs.editJob": "Edit Job",
17
+ "jobs.runNow": "Run now",
18
+ "jobs.newSchedule": "Create schedule",
19
+ "jobs.editSchedule": "Edit schedule",
20
+ "jobs.cancelRun": "Cancel this Run",
21
+ "jobs.disable": "Disable future runs",
22
+ "jobs.enable": "Enable schedule",
23
+ "jobs.refresh": "Refresh facts",
24
+ "jobs.save": "Save",
25
+ "jobs.close": "Close editor",
26
+ "jobs.discard": "Discard this unsaved draft?",
27
+ "jobs.loading": "Reading authoritative state…",
28
+ "jobs.submitting": "Submitting…",
29
+ "jobs.saved": "Saved. This is not proof of execution or success.",
30
+ "jobs.admitted": "Run admitted",
31
+ "jobs.overlap": "Existing Run occupies this Job; no new Run admitted",
32
+ "jobs.cancelRequested": "Cancellation requested. Inspect the exact Run for confirmed termination.",
33
+ "jobs.observed": "Facts refreshed",
34
+ "jobs.unknown": "Submission outcome is unknown; it will not be resent. Inspect current facts and the original operation before taking another action.",
35
+ "jobs.empty": "No Jobs in this scope. Creating a Job does not run it.",
36
+ "jobs.configuration": "Execution configuration",
37
+ "jobs.taskConfig": "Task-only shell specification: use the registered managed workspace, exact Project HEAD and owner. Commands and environment must not contain secrets. The server validates scope and execution prerequisites.",
38
+ "jobs.rebind": "Saving a Project refresh Job explicitly binds its current catalog target. It does not refresh the Project. No arbitrary global shell commands.",
39
+ "jobs.rule": "Time rule",
40
+ "jobs.instant": "Instant / interval anchor (ISO time with UTC offset)",
41
+ "jobs.every": "Interval (milliseconds, minimum 1000)",
42
+ "jobs.cron": "Cron expression (five fields)",
43
+ "jobs.timezone": "Timezone",
44
+ "jobs.scheduleHelp": "Creating a schedule enables future execution. Rule edits preserve enabled status only when already enabled. Cron uses the service's DST rules; next due is never calculated in this browser.",
45
+ "jobs.state": "State",
46
+ "jobs.recentRun": "Most recent Run",
47
+ "jobs.refreshAttempt": "Project: last attempt",
48
+ "jobs.refreshSuccess": "Project: last successful refresh",
49
+ "jobs.onceCompleted": "The once plan was admitted. Completed does not mean its Run succeeded.",
50
+ "jobs.revision": "Revision",
51
+ "jobs.schedules": "Schedules",
52
+ "jobs.noSchedules": "No schedules. Immediate execution is still available.",
53
+ "jobs.nextDue": "Next due (UTC; enabled plans only)",
54
+ "jobs.lastAttempt": "Last admission attempt",
55
+ "jobs.lastSuccess": "Last successful scheduled Run",
56
+ "jobs.refreshFacts": "Project refresh: last attempt / last success",
57
+ "jobs.history": "Execution history · exact Runs",
58
+ "jobs.noRuns": "No executions yet.",
59
+ "jobs.inspectRun": "Inspect attempt Run",
60
+ "jobs.trigger": "Trigger",
61
+ "jobs.created": "Created",
62
+ "jobs.finished": "Finished",
63
+ "jobs.snapshot": "Exact Run · target snapshot, result and process evidence",
64
+ "jobs.logs": "Step logs",
65
+ "jobs.truncated": "last 64 KiB",
66
+ "jobs.noLogs": "No checkpointed step logs yet. This does not establish success.",
6
67
  "models.subscriptionAuth": "Subscription",
7
68
  "models.account": "Account",
8
69
  "models.providerReadOnlyHelp": "Managed by its provider. Edit the configuration in the owning service.",
@@ -991,7 +1052,7 @@ export const EN = {
991
1052
  "messageKind.operator": "Operator",
992
1053
  "messageKind.role-result": "Role result",
993
1054
  "messageKind.system": "System",
994
- "remoteDelivery.none": "No code delivery",
1055
+ "remoteDelivery.none": "No delivery obligation",
995
1056
  "remoteDelivery.unavailable": "Exact heads unavailable",
996
1057
  "remoteDelivery.pending": "Not delivered",
997
1058
  "remoteDelivery.uncovered": "Merged, acceptance not covered",
@@ -1023,7 +1084,9 @@ export const EN = {
1023
1084
  "delivery.title": "Integration and delivery",
1024
1085
  "delivery.hint": "Integration checks, PR/MR and remote merge are separate facts; none of them is acceptance.",
1025
1086
  "delivery.readAt": "Evidence read at",
1026
- "delivery.counts": "{merged}/{total} code projects merged · {verified}/{total} verified",
1087
+ "delivery.counts": "{merged}/{total} required code projects merged · {verified}/{total} verified",
1088
+ "delivery.excluded": "Excluded from delivery",
1089
+ "delivery.scopeDecision": "Scope decision",
1027
1090
  "delivery.noProjects": "No project is bound to this Task.",
1028
1091
  "evidence.loading": "Reading original evidence…",
1029
1092
  "evidence.report": "Original review report",
@@ -1,6 +1,67 @@
1
1
  // Simplified Chinese UI copy. Typed against the English catalog, so a key that
2
2
  // does not exist in en.ts is a compile error; a missing key falls back to en.
3
3
  export const ZH_CN = {
4
+ "tabs.jobs": "Job",
5
+ "settings.section.jobs": "Job 与时间计划",
6
+ "jobs.heading": "Job 与时间计划",
7
+ "jobs.help": "立即运行只新增一次执行,不改变周期;取消仅处理选中的 Run;停用计划阻止后续触发。操作使用当前 Operator 绑定。",
8
+ "jobs.details": "详情",
9
+ "jobs.scope": "所属范围",
10
+ "jobs.project": "目标 Project",
11
+ "jobs.name": "名称",
12
+ "jobs.action": "受控操作",
13
+ "jobs.newJob": "创建 Job",
14
+ "jobs.editJob": "编辑 Job",
15
+ "jobs.runNow": "立即运行",
16
+ "jobs.newSchedule": "创建时间计划",
17
+ "jobs.editSchedule": "编辑时间计划",
18
+ "jobs.cancelRun": "取消本次 Run",
19
+ "jobs.disable": "停用后续计划",
20
+ "jobs.enable": "启用时间计划",
21
+ "jobs.refresh": "刷新事实",
22
+ "jobs.save": "保存",
23
+ "jobs.close": "关闭编辑",
24
+ "jobs.discard": "放弃当前未保存的草稿?",
25
+ "jobs.loading": "正在读取权威状态…",
26
+ "jobs.submitting": "提交中…",
27
+ "jobs.saved": "已保存;不代表已执行或执行成功。",
28
+ "jobs.admitted": "已准入 Run",
29
+ "jobs.overlap": "已有 Run 占用此 Job,未新增执行",
30
+ "jobs.cancelRequested": "已请求取消;请查看准确 Run 确认是否终止。",
31
+ "jobs.observed": "事实已刷新",
32
+ "jobs.unknown": "提交结果未知,不会重发。请先检查当前事实和原操作,再决定后续动作。",
33
+ "jobs.empty": "此范围暂无 Job。创建定义不会立即执行。",
34
+ "jobs.configuration": "执行配置",
35
+ "jobs.taskConfig": "仅 Task 可配置 shell:使用已登记的受管工作区、准确 Project HEAD 和 owner。命令与环境变量不得包含凭据;服务端校验范围和执行前提。",
36
+ "jobs.rebind": "保存 Project 刷新 Job 会显式绑定当前目录中的目标,但不会执行刷新。全局不开放任意 shell 命令。",
37
+ "jobs.rule": "时间规则",
38
+ "jobs.instant": "执行时间/周期锚点(带 UTC 偏移的 ISO 时间)",
39
+ "jobs.every": "周期间隔(毫秒,至少 1000)",
40
+ "jobs.cron": "Cron 表达式(五字段)",
41
+ "jobs.timezone": "时区",
42
+ "jobs.scheduleHelp": "创建计划会启用后续执行;修改规则只在原本启用时保持启用。Cron 遵循服务端夏令时规则,浏览器不计算下次时间。",
43
+ "jobs.state": "状态",
44
+ "jobs.recentRun": "最近一次 Run",
45
+ "jobs.refreshAttempt": "Project 最近尝试",
46
+ "jobs.refreshSuccess": "Project 最近成功刷新",
47
+ "jobs.onceCompleted": "一次性计划已准入;completed 不代表其 Run 已成功。",
48
+ "jobs.revision": "修订",
49
+ "jobs.schedules": "时间计划",
50
+ "jobs.noSchedules": "暂无时间计划,仍可立即运行。",
51
+ "jobs.nextDue": "下次时间(UTC,仅启用时触发)",
52
+ "jobs.lastAttempt": "最近准入尝试",
53
+ "jobs.lastSuccess": "最近成功的计划 Run",
54
+ "jobs.refreshFacts": "Project 刷新:最近尝试/最近成功",
55
+ "jobs.history": "执行历史 · 精确 Run",
56
+ "jobs.noRuns": "尚无执行记录。",
57
+ "jobs.inspectRun": "查看此次尝试的 Run",
58
+ "jobs.trigger": "触发来源",
59
+ "jobs.created": "创建时间",
60
+ "jobs.finished": "结束时间",
61
+ "jobs.snapshot": "准确 Run · 目标快照、结果与进程证据",
62
+ "jobs.logs": "步骤日志",
63
+ "jobs.truncated": "末尾 64 KiB",
64
+ "jobs.noLogs": "暂无已写入检查点的步骤日志;这不代表成功。",
4
65
  "models.subscriptionAuth": "官方订阅",
5
66
  "models.account": "账号",
6
67
  "models.providerReadOnlyHelp": "由来源服务管理,请在对应服务中修改配置。",
@@ -987,7 +1048,7 @@ export const ZH_CN = {
987
1048
  "messageKind.operator": "Operator",
988
1049
  "messageKind.role-result": "Role 结果",
989
1050
  "messageKind.system": "系统",
990
- "remoteDelivery.none": "无代码交付",
1051
+ "remoteDelivery.none": "无交付义务",
991
1052
  "remoteDelivery.unavailable": "精确 head 不可用",
992
1053
  "remoteDelivery.pending": "尚未交付",
993
1054
  "remoteDelivery.uncovered": "已合并,未覆盖验收",
@@ -1019,7 +1080,9 @@ export const ZH_CN = {
1019
1080
  "delivery.title": "集成与交付",
1020
1081
  "delivery.hint": "集成检查、PR/MR 与远程合并是不同事实,都不等于验收。",
1021
1082
  "delivery.readAt": "证据读取于",
1022
- "delivery.counts": "已合入 {merged}/{total} 个代码项目 · 已验证 {verified}/{total}",
1083
+ "delivery.counts": "已合入 {merged}/{total} 个应交付代码项目 · 已验证 {verified}/{total}",
1084
+ "delivery.excluded": "已排除交付",
1085
+ "delivery.scopeDecision": "交付范围决定",
1023
1086
  "delivery.noProjects": "此任务未绑定项目。",
1024
1087
  "evidence.loading": "正在读取原始证据…",
1025
1088
  "evidence.report": "原始评审报告",
@@ -60,6 +60,7 @@ import { SELECTION_SCRIPT } from "./app/selection.js";
60
60
  import { TASK_VIEW_SCRIPT } from "./app/taskView.js";
61
61
  import { SETTINGS_SCRIPT } from "./app/settings.js";
62
62
  import { SKILLS_SCRIPT } from "./views/skills.js";
63
+ import { JOBS_SCRIPT } from "./views/jobs.js";
63
64
  import { MODELS_VIEW_SCRIPT } from "./views/models.js";
64
65
  import { MODELS_SCRIPT } from "./app/models.js";
65
66
  import { MANAGE_SHELL_SCRIPT } from "./layout/manageShell.js";
@@ -67,6 +68,7 @@ import { MANAGE_SCRIPT } from "./app/manage.js";
67
68
  export { APP_SCRIPT } from "./app/main.js";
68
69
  export const CLIENT_MODULES = Object.freeze({
69
70
  "views/skills": SKILLS_SCRIPT,
71
+ "views/jobs": JOBS_SCRIPT,
70
72
  "views/models": MODELS_VIEW_SCRIPT,
71
73
  "lib/dom": DOM_SCRIPT,
72
74
  "lib/format": FORMAT_SCRIPT,
@@ -0,0 +1,222 @@
1
+ export const JOBS_SCRIPT = String.raw `
2
+ import { h } from "/assets/js/lib/dom.js";
3
+ import { requestJson, submitMutation } from "/assets/js/lib/api.js";
4
+
5
+ // One view for two server-fixed scopes. Refresh only reads facts; it never
6
+ // admits due work, derives nextDue or replaces a selected exact Run with latest.
7
+ export function mountJobs(root, t, taskId) {
8
+ const base = taskId ? "/api/tasks/" + encodeURIComponent(taskId) : "/api/settings";
9
+ const copy = key => t("jobs." + key);
10
+ const receipt = h("p.settings-receipt", { role: "status", "aria-live": "polite" });
11
+ const list = h("div.jobs-list");
12
+ const detail = h("section.jobs-detail", { "aria-label": copy("details") });
13
+ const editor = h("section");
14
+ let jobs = [], schedules = [], selected, selectedRun, generation = 0, pending = false, dirty = false;
15
+ const blocked = new Set();
16
+ const read = (family, input) => requestJson(base + "/" + family + "?" + new URLSearchParams(input));
17
+ const button = (text, action, primary = false) => h("button.btn" + (primary ? ".btn-primary" : ""), {
18
+ type: "button", onclick: action
19
+ }, text);
20
+ const json = value => h("pre.settings-observation", null, JSON.stringify(value, null, 2));
21
+ const fact = (name, value) => h("p.job-fact", null, h("strong", null, name + ": "), String(value ?? "—"));
22
+ const raw = (name, value) => h("details", null, h("summary", null, name), json(value));
23
+ function field(name, control) { return h("label.field", null, h("span", null, name), control); }
24
+ function input(value = "", attrs = {}) { return h("input", { value, ...attrs }); }
25
+ function choose(values, value) {
26
+ const node = h("select", null, values.map(v => h("option", { value: v }, v)));
27
+ node.value = value || values[0]; return node;
28
+ }
29
+ function guardDraft() { return !dirty || window.confirm(copy("discard")); }
30
+ function closeEditor() { editor.replaceChildren(); dirty = false; root.dataset.unsent = "false"; }
31
+ function status(text, error = false) { receipt.textContent = text; receipt.classList.toggle("settings-error", error); }
32
+ async function mutate(family, payload, after) {
33
+ const key = base + "/" + family + "/" + payload.id;
34
+ if (pending || blocked.has(key)) { status(copy("unknown"), true); return; }
35
+ pending = true;
36
+ root.dataset.reading = "true";
37
+ const controls = [...root.querySelectorAll("button,input,select,textarea")];
38
+ const disabled = controls.map(node => node.disabled);
39
+ controls.forEach(node => { node.disabled = true; });
40
+ status(copy("submitting"));
41
+ try {
42
+ const result = await submitMutation(key, base + "/" + family, payload);
43
+ status(payload.op === "run"
44
+ ? (result.reason === "overlap" ? copy("overlap") : copy("admitted")) + " · " + result.run.id + " · " + result.run.status
45
+ : payload.op === "run-cancel" ? copy("cancelRequested") : copy("saved"));
46
+ if (after) after(result);
47
+ await refresh(false);
48
+ } catch (error) {
49
+ if (error.disposition !== "not-submitted") blocked.add(key);
50
+ status(error.message + (blocked.has(key) ? "\n" + copy("unknown") : ""), true);
51
+ } finally {
52
+ pending = false;
53
+ root.dataset.reading = "false";
54
+ controls.forEach((node, index) => { node.disabled = disabled[index]; });
55
+ }
56
+ }
57
+ function form(title, contents, submit) {
58
+ if (!guardDraft()) return;
59
+ closeEditor();
60
+ const node = h("form.settings-card.settings-stack", null, h("h3", null, title), contents,
61
+ h("div.settings-actions", null, h("button.btn.btn-primary", { type: "submit" }, copy("save")),
62
+ button(copy("close"), () => { if (guardDraft()) closeEditor(); })));
63
+ const markDirty = () => { dirty = true; root.dataset.unsent = "true"; };
64
+ node.addEventListener("input", markDirty);
65
+ node.addEventListener("change", markDirty);
66
+ node.addEventListener("submit", event => {
67
+ event.preventDefault();
68
+ try { submit(); } catch (error) { status(error.message, true); }
69
+ });
70
+ editor.append(node);
71
+ node.querySelector("input,select,textarea")?.focus();
72
+ }
73
+ function editJob(job) {
74
+ const id = input(job?.id || "", { required: true, pattern: "[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", disabled: !!job });
75
+ const title = input(job?.title || "", { required: true });
76
+ const action = choose(["system.noop", "project.refresh"], job?.configuration.action);
77
+ const project = input(job?.configuration.projectId || "");
78
+ const configuration = h("textarea", { rows: 12, required: true });
79
+ configuration.value = JSON.stringify(job?.configuration || { kind: "shell", owner: { kind: "task" },
80
+ projectId: "", head: "", workspace: "", env: {}, steps: [{ name: "check", command: "true" }] }, null, 2);
81
+ const fields = [field("ID", id), field(copy("name"), title)];
82
+ if (taskId) fields.push(h("p", null, copy("taskConfig")), field(copy("configuration"), configuration));
83
+ else {
84
+ fields.push(field(copy("action"), action), field(copy("project"), project), h("p", null, copy("rebind")));
85
+ const sync = () => { project.disabled = action.value !== "project.refresh"; project.required = !project.disabled; };
86
+ action.addEventListener("change", sync); sync();
87
+ }
88
+ form(job ? copy("editJob") : copy("newJob"), fields, () => {
89
+ const config = taskId ? JSON.parse(configuration.value) : {
90
+ kind: "builtin", action: action.value, input: {},
91
+ ...(action.value === "project.refresh" ? { projectId: project.value.trim() } : {})
92
+ };
93
+ void mutate("jobs", { op: job ? "update" : "define", id: id.value.trim(), title: title.value.trim(),
94
+ configuration: config, ...(job ? { expectedRevision: job.revision } : {}) }, result => {
95
+ selected = result.id; selectedRun = undefined; closeEditor();
96
+ });
97
+ });
98
+ }
99
+ function editSchedule(schedule) {
100
+ const id = input(schedule?.id || "", { required: true, disabled: !!schedule });
101
+ const jobId = choose(jobs.map(j => j.id), schedule?.jobId || selected);
102
+ const kind = choose(["once", "interval", "cron"], schedule?.rule.kind || "interval");
103
+ const rule = schedule?.rule;
104
+ 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 });
106
+ const expression = input(rule?.expression || "0 9 * * *");
107
+ const timezone = input(rule?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC");
108
+ const when = field(copy("instant"), at), interval = field(copy("every"), every);
109
+ const cron = field(copy("cron"), expression), zone = field(copy("timezone"), timezone);
110
+ const sync = () => {
111
+ when.hidden = kind.value === "cron"; interval.hidden = kind.value !== "interval";
112
+ cron.hidden = zone.hidden = kind.value !== "cron"; at.required = !when.hidden;
113
+ };
114
+ kind.addEventListener("change", sync); sync();
115
+ form(schedule ? copy("editSchedule") : copy("newSchedule"),
116
+ [field("ID", id), field("Job", jobId), field(copy("rule"), kind), when, interval, cron, zone,
117
+ h("p", null, copy("scheduleHelp"))], () => {
118
+ const rule = kind.value === "once" ? { kind: "once", at: at.value }
119
+ : kind.value === "interval" ? { kind: "interval", anchor: at.value, everyMs: Number(every.value) }
120
+ : { kind: "cron", expression: expression.value, timezone: timezone.value };
121
+ void mutate("schedules", { op: schedule ? "update" : "create", id: id.value.trim(), jobId: jobId.value, rule,
122
+ ...(schedule ? { expectedRevision: schedule.revision } : {}) }, closeEditor);
123
+ });
124
+ }
125
+ async function showRun(jobId, runId, target) {
126
+ selectedRun = runId;
127
+ target.replaceChildren(h("p", null, copy("loading")));
128
+ try {
129
+ const [run, logs] = await Promise.all([read("jobs", { op: "run-get", id: jobId, runId }),
130
+ read("jobs", { op: "logs", id: jobId, runId })]);
131
+ if (selected !== jobId || selectedRun !== runId) return;
132
+ target.replaceChildren(h("h4", null, run.id), fact(copy("state"), run.status),
133
+ fact(copy("trigger"), run.trigger.source + " · " + run.trigger.key),
134
+ 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")));
139
+ if (["queued", "running", "unknown-needs-attention"].includes(run.status)) target.prepend(
140
+ button(copy("cancelRun"), () => void mutate("jobs", { op: "run-cancel", id: jobId, runId })));
141
+ } catch (error) { target.replaceChildren(h("p.settings-error", null, error.message)); }
142
+ }
143
+ async function showDetail(epoch) {
144
+ const job = jobs.find(j => j.id === selected);
145
+ if (!job) { detail.replaceChildren(); return; }
146
+ const [runs, plans] = await Promise.all([
147
+ read("jobs", { op: "runs", id: job.id }),
148
+ Promise.all(schedules.filter(s => s.jobId === job.id).map(s => read("schedules", { op: "show", id: s.id })))
149
+ ]);
150
+ if (epoch !== generation) return;
151
+ runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
152
+ const summary = run => run ? (run.id || run.runId) + " · " + run.status + " · "
153
+ + (run.terminalAt || run.finishedAt || run.createdAt || run.attemptedAt) : null;
154
+ const runDetail = h("section.job-run");
155
+ detail.replaceChildren(h("div.settings-stack", null, h("h3", null, job.title), fact("ID", job.id),
156
+ fact(copy("scope"), taskId || "global"), fact(copy("project"), job.configuration.projectId),
157
+ fact(copy("revision"), job.revision),
158
+ fact(copy("recentRun"), summary(runs[0])),
159
+ job.refresh ? h("div", null,
160
+ fact(copy("refreshAttempt"), summary(job.refresh.lastAttempt)),
161
+ fact(copy("refreshSuccess"), summary(job.refresh.lastSuccessfulRefresh))) : null,
162
+ h("div.settings-actions", null,
163
+ button(copy("runNow"), () => void mutate("jobs", { op: "run", id: job.id, requestId: crypto.randomUUID() }, result => {
164
+ selectedRun = result.run.id;
165
+ }), true),
166
+ button(copy("editJob"), () => editJob(job)),
167
+ button(copy("newSchedule"), () => editSchedule())),
168
+ raw(copy("configuration"), job.configuration),
169
+ job.refresh ? raw(copy("refreshFacts"), job.refresh) : null,
170
+ h("h3", null, copy("schedules")),
171
+ plans.length ? h("div.settings-stack", null, plans.map(({ schedule: s, lastSuccess }) => h("article.settings-card", null,
172
+ h("h4", null, s.id), fact(copy("state"), s.state),
173
+ s.state === "completed" ? h("p", null, copy("onceCompleted")) : null,
174
+ 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
+ fact(copy("timezone"), s.rule.timezone || "UTC / offset"),
176
+ fact(copy("nextDue"), s.nextDue), fact(copy("lastAttempt"), s.lastAttempt
177
+ ? s.lastAttempt.at + " · " + s.lastAttempt.outcome + " · " + (s.lastAttempt.runId || s.lastAttempt.reason || "") : null),
178
+ fact(copy("lastSuccess"), lastSuccess ? lastSuccess.id + " · " + (lastSuccess.terminalAt || lastSuccess.updatedAt) : null),
179
+ s.reason ? h("p.settings-error", null, s.reason) : null,
180
+ h("div.settings-actions", null,
181
+ 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 ? button(copy("inspectRun"), () => void showRun(job.id, s.lastAttempt.runId, runDetail)) : null),
185
+ raw(copy("details"), s)))) : h("p", null, copy("noSchedules")),
186
+ h("h3", null, copy("history")),
187
+ runs.length ? h("div.jobs-runs", null, runs.map(run =>
188
+ button(run.id + " · " + run.status + " · " + run.trigger.source + " · " + run.createdAt,
189
+ () => void showRun(job.id, run.id, runDetail)))) : h("p", null, copy("noRuns")),
190
+ runDetail));
191
+ if (selectedRun) await showRun(job.id, selectedRun, runDetail);
192
+ }
193
+ async function refresh(announce = true) {
194
+ const epoch = ++generation;
195
+ if (announce) status(copy("loading"));
196
+ try {
197
+ const result = await Promise.all([read("jobs", { op: "list" }), read("schedules", { op: "list" })]);
198
+ if (epoch !== generation) return;
199
+ [jobs, schedules] = result;
200
+ if (!selected && jobs.length) selected = jobs[0].id;
201
+ list.replaceChildren(...(jobs.length ? jobs.map(job => h("button.job-choice", {
202
+ type: "button", "aria-pressed": String(selected === job.id), onclick: () => {
203
+ if (pending || !guardDraft()) return;
204
+ closeEditor(); selected = job.id; selectedRun = undefined; void refresh();
205
+ }
206
+ }, h("strong", null, job.title), h("code", null, job.id),
207
+ h("small", null, job.configuration.action || job.configuration.kind),
208
+ h("small", null, job.configuration.projectId || "—"))) : [h("p", null, copy("empty"))]));
209
+ await showDetail(epoch);
210
+ if (announce && epoch === generation) status(copy("observed") + " · " + new Date().toISOString());
211
+ } catch (error) { if (epoch === generation) status(error.message, true); }
212
+ }
213
+ root.classList.add("jobs-manager");
214
+ root.replaceChildren(h("h2", null, copy("heading")),
215
+ h("p", null, copy("help")), fact(copy("scope"), taskId || "global"),
216
+ h("div.settings-actions", null, button(copy("newJob"), () => editJob(), true),
217
+ button(copy("refresh"), () => { if (!pending) void refresh(); })),
218
+ receipt, editor, h("div.jobs-layout", null, list, detail));
219
+ void refresh();
220
+ return { isDirty: () => dirty, isBusy: () => pending, refresh };
221
+ }
222
+ `;
@@ -53,9 +53,9 @@ function drawResults(body, scope) {
53
53
  function drawRemote(body, remote, t) {
54
54
  body.append(h("h4.sub-head", null, icon("package", "icon-sm"), h("span", null, t("evidence.remote")),
55
55
  badge(t("remoteDelivery." + remote.status, remote.status), DELIVERY_TONES[remote.status] || "idle", { dot: true })));
56
- if (remote.codeProjectCount) {
56
+ if (remote.requiredCodeProjectCount) {
57
57
  body.append(h("p.faint.small", null, fill(t("delivery.counts"), {
58
- merged: remote.mergedProjectCount, verified: remote.verifiedProjectCount, total: remote.codeProjectCount
58
+ merged: remote.mergedProjectCount, verified: remote.verifiedProjectCount, total: remote.requiredCodeProjectCount
59
59
  })));
60
60
  }
61
61
  (remote.projects || []).forEach(function (project) { body.append(projectRow(project, t)); });
@@ -85,6 +85,7 @@ function projectRow(project, t) {
85
85
  const href = publication && publication.externalUrl && safeUrl(publication.externalUrl);
86
86
  const element = h("article.record.delivery-project");
87
87
  element.append(h("header.record-head", null, icon("folder", "icon-sm"), h("strong", null, project.directory || project.projectId),
88
+ project.deliveryRequirement === "excluded" ? chip(t("delivery.excluded")) : null,
88
89
  project.coverage ? chip(label(t, "coverage", project.coverage)) : null,
89
90
  h("span.spacer"),
90
91
  publication ? (href
@@ -97,6 +98,9 @@ function projectRow(project, t) {
97
98
  project.state ? chip(label(t, "publicationState", project.state)) : null,
98
99
  project.verification ? chip(t("evidence.verification") + " · " + label(t, "verification", project.verification)) : null,
99
100
  project.adoption ? h("span", null, t("evidence.adoption") + " ", mono(project.adoption.id)) : null));
101
+ if (project.scopeDecision) element.append(h("p.faint.small", null,
102
+ t("delivery.scopeDecision") + " " + project.scopeDecision.id + " · "
103
+ + project.scopeDecision.payload.sourceMessageId + " · " + project.scopeDecision.payload.reason));
100
104
  return element;
101
105
  }
102
106
 
@@ -12,6 +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
16
 
16
17
  // Overview: where it stands. Delivery: what it produced. Runtime: what is
17
18
  // live now. Records: what happened. Retired section names stay addressable.
@@ -20,9 +21,12 @@ export const TAB_ALIASES = ${JSON.stringify(TASK_TAB_ALIASES)};
20
21
  const TAB_ICONS = ${JSON.stringify(TASK_TAB_ICONS)};
21
22
 
22
23
  export function renderTaskDetail(container, data, t, locale, ctx) {
24
+ const savedJobs = container.dataset.taskId === data.task.id && container.dataset.jobsLocale === locale
25
+ ? container.querySelector("#panel-jobs") : null;
23
26
  clear(container);
24
27
  const task = data.task;
25
28
  container.dataset.taskId = task.id;
29
+ container.dataset.jobsLocale = locale;
26
30
  data.viewState.coreKey = data.core.coreCursor;
27
31
  const page = h("div.page.task-page");
28
32
  page.append(h("header.task-header", null, taskBar(task, t, ctx)),
@@ -30,10 +34,16 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
30
34
  h("div.tabs-wrap", null, taskTabs(data.core, t, ctx)));
31
35
  const panels = {};
32
36
  TABS.forEach(function (tab) {
33
- panels[tab] = h("section.tab-panel", { id: "panel-" + tab, role: "tabpanel", "aria-labelledby": "tab-" + tab, hidden: ctx.activeTab !== tab });
37
+ panels[tab] = tab === "jobs" && savedJobs ? savedJobs
38
+ : h("section.tab-panel", { id: "panel-" + tab, role: "tabpanel", "aria-labelledby": "tab-" + tab });
39
+ panels[tab].hidden = ctx.activeTab !== tab;
34
40
  page.append(panels[tab]);
35
41
  });
36
42
  renderOverviewPanel(panels.overview, data, t, locale, ctx);
43
+ if (!savedJobs) panels.jobs.onShow = () => {
44
+ panels.jobs.onShow = null;
45
+ mountJobs(panels.jobs, t, task.id);
46
+ };
37
47
  [["delivery", renderDelivery], ["runtime", renderRuntime], ["records", renderRecords]].forEach(function (entry) {
38
48
  const panel = panels[entry[0]];
39
49
  panel.onShow = function () {
@@ -7,6 +7,7 @@ export const MANAGE_SECTIONS = Object.freeze({
7
7
  settings: [
8
8
  { page: "settings", section: "preferences", key: "settings.section.preferences", label: "Preferences" },
9
9
  { page: "settings", section: "skills", key: "settings.section.skills", label: "Skills" },
10
+ { page: "settings", section: "jobs", key: "settings.section.jobs", label: "Jobs & schedules" },
10
11
  { page: "settings", section: "server", key: "settings.section.server", label: "Agents & server" },
11
12
  { page: "models", section: "connection", key: "models.section.connection", label: "Model connection" }
12
13
  ]
@@ -1,8 +1,8 @@
1
1
  // Task page sections, shared by the shell's shortcut list and the client.
2
2
  // Overview: where it stands. Delivery: what it produced. Runtime: what is live
3
3
  // now. Records: what happened. Retired section names stay addressable.
4
- export const TASK_TABS = ["overview", "delivery", "runtime", "records"];
4
+ export const TASK_TABS = ["overview", "delivery", "runtime", "records", "jobs"];
5
5
  export const TASK_TAB_ICONS = {
6
- overview: "target", delivery: "package", runtime: "pulse", records: "history"
6
+ overview: "target", delivery: "package", runtime: "pulse", records: "history", jobs: "clock"
7
7
  };
8
8
  export const TASK_TAB_ALIASES = Object.freeze({ work: "delivery", execution: "runtime", history: "records", details: "records" });
@@ -22,6 +22,9 @@ export const SETTINGS_MAIN = `
22
22
  ${manageHead("settings-skills-title", "settings.section.skills", "Skills", "skills.browser.intro", "Read existing packages and choose additional bindings for one explicit Role. Scripts are displayed as data, never run.")}
23
23
  <section id="skill-browser" class="card settings-card settings-stack"></section>
24
24
  </section>
25
+ <section class="manage-section" data-section="jobs" hidden>
26
+ <section id="global-jobs"></section>
27
+ </section>
25
28
  <section class="manage-section" data-section="server" aria-labelledby="settings-server-title" hidden>
26
29
  ${manageHead("settings-server-title", "settings.section.server", "Agents & server", "settings.hint.server", "Desired configuration for Yui, Roles, Agents and Profiles. Saving targets later launches; it is not proof that existing Sessions adopted it.")}
27
30
  <label class="field settings-search"><span data-i18n="settings.search">Find a setting</span><input id="settings-search" type="search" autocomplete="off"></label>
@@ -18,6 +18,7 @@ import { TERMINAL_STYLES } from "./views/terminal.js";
18
18
  import { MANAGE_STYLES } from "./views/manage.js";
19
19
  import { SETTINGS_STYLES } from "./views/settings.js";
20
20
  import { MODELS_STYLES } from "./views/models.js";
21
+ import { JOBS_STYLES } from "./views/jobs.js";
21
22
  // The stylesheet registry, in cascade order: foundations (fonts, tokens,
22
23
  // element defaults), the workspace frame, shared UI primitives, then one
23
24
  // sheet per view, rendered prose, and breakpoints last so they override
@@ -42,6 +43,7 @@ export const STYLESHEETS = Object.freeze([
42
43
  { name: "views/manage", body: MANAGE_STYLES },
43
44
  { name: "views/settings", body: SETTINGS_STYLES },
44
45
  { name: "views/models", body: MODELS_STYLES },
46
+ { name: "views/jobs", body: JOBS_STYLES },
45
47
  { name: "markdown", body: MARKDOWN_STYLES },
46
48
  { name: "layout/responsive", body: RESPONSIVE_STYLES }
47
49
  ]);
@@ -0,0 +1,19 @@
1
+ export const JOBS_STYLES = `
2
+ .jobs-manager { min-width: 0; display: grid; gap: 16px; container-type: inline-size; }
3
+ .jobs-layout { display: grid; grid-template-columns: minmax(160px, 240px) minmax(0, 1fr); gap: 24px; align-items: start; }
4
+ .jobs-list, .jobs-detail, .jobs-runs { display: grid; gap: 12px; min-width: 0; }
5
+ .job-choice { display: grid; gap: 6px; text-align: left; padding: 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: inherit; cursor: pointer; overflow-wrap: anywhere; }
6
+ .job-choice[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
7
+ .job-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
8
+ .jobs-manager .settings-actions { flex-wrap: wrap; }
9
+ .jobs-manager .settings-observation { max-width: 100%; max-height: 420px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
10
+ .jobs-manager .btn, .job-fact { overflow-wrap: anywhere; white-space: normal; text-align: left; }
11
+ .jobs-runs .btn { font-family: var(--font-mono); font-size: 12px; }
12
+ .jobs-manager textarea { width: 100%; font-family: var(--font-mono); }
13
+ .jobs-manager [hidden] { display: none !important; }
14
+ @media (max-width: 720px) { .jobs-layout { grid-template-columns: minmax(0, 1fr); } }
15
+ @container (max-width: 640px) {
16
+ .jobs-layout { grid-template-columns: minmax(0, 1fr); }
17
+ .jobs-list { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
18
+ }
19
+ `;