@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
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { requireKnownFields } from "../domain/validation.js";
|
|
2
|
+
import { nextScheduleDue, scheduleId, validateSchedule } from "../schedule/schedule.js";
|
|
3
|
+
import { admitJobRun, authorizeDefinition, authorizeScope, createJobDefinitionControl } from "./jobDefinitionControl.js";
|
|
4
|
+
import { jobAuthorityBinding } from "./jobControl.js";
|
|
5
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
6
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
7
|
+
import { enqueueSupervisorEvent } from "../scheduler/operatorEvent.js";
|
|
8
|
+
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
9
|
+
function binding(store, caller, scope) {
|
|
10
|
+
return jobAuthorityBinding(store, caller.scope, caller.role, scope.kind === "task" ? scope.taskId : "");
|
|
11
|
+
}
|
|
12
|
+
function authorizeSchedule(store, schedule, caller, write) {
|
|
13
|
+
const job = store.getJobDefinition(schedule.scope, schedule.jobId);
|
|
14
|
+
if (job === null)
|
|
15
|
+
throw new Error("Schedule Job definition is missing.");
|
|
16
|
+
authorizeDefinition(store, job, caller, write);
|
|
17
|
+
return job;
|
|
18
|
+
}
|
|
19
|
+
function fence(schedule, now) {
|
|
20
|
+
return new Date(Math.max(now.getTime(), Date.parse(schedule?.processedThrough ?? "") || 0));
|
|
21
|
+
}
|
|
22
|
+
export function createScheduleControl(store, home) {
|
|
23
|
+
return (value, now) => {
|
|
24
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
throw new Error("Invalid Schedule request.");
|
|
26
|
+
requireKnownFields(value, ["op", "scope", "caller", "id", "jobId", "rule", "expectedRevision"], "Schedule request");
|
|
27
|
+
const input = value;
|
|
28
|
+
authorizeScope(store, input.scope, input.caller);
|
|
29
|
+
return store.transaction(tx => {
|
|
30
|
+
if (input.op === "list") {
|
|
31
|
+
return tx.listSchedules(input.scope).filter(s => {
|
|
32
|
+
try {
|
|
33
|
+
authorizeSchedule(tx, s, input.caller, false);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
scheduleId(input.id);
|
|
42
|
+
const previous = tx.getSchedule(input.scope, input.id);
|
|
43
|
+
if (input.op === "create") {
|
|
44
|
+
scheduleId(input.jobId);
|
|
45
|
+
const job = tx.getJobDefinition(input.scope, input.jobId);
|
|
46
|
+
if (job === null)
|
|
47
|
+
throw new Error("Schedule Job definition is missing.");
|
|
48
|
+
if (previous !== null) {
|
|
49
|
+
authorizeSchedule(tx, previous, input.caller, false);
|
|
50
|
+
if (previous.jobId !== input.jobId || JSON.stringify(previous.rule) !== JSON.stringify(input.rule)) {
|
|
51
|
+
throw new Error("Schedule id already exists; use update with its revision.");
|
|
52
|
+
}
|
|
53
|
+
return previous;
|
|
54
|
+
}
|
|
55
|
+
authorizeDefinition(tx, job, input.caller, true);
|
|
56
|
+
const due = nextScheduleDue(input.rule, now);
|
|
57
|
+
if (due === null)
|
|
58
|
+
throw new Error("A new once Schedule must be in the future.");
|
|
59
|
+
const schedule = validateSchedule({
|
|
60
|
+
schemaVersion: 1, id: input.id, scope: structuredClone(input.scope), jobId: input.jobId,
|
|
61
|
+
rule: structuredClone(input.rule), state: "enabled", revision: 1, generation: 1, nextDue: due,
|
|
62
|
+
authorization: { caller: structuredClone(input.caller), binding: binding(tx, input.caller, input.scope), jobRevision: job.revision },
|
|
63
|
+
createdAt: now.toISOString(), updatedAt: now.toISOString()
|
|
64
|
+
});
|
|
65
|
+
tx.saveSchedule(schedule);
|
|
66
|
+
return schedule;
|
|
67
|
+
}
|
|
68
|
+
if (previous === null)
|
|
69
|
+
throw new Error("Schedule not found.");
|
|
70
|
+
authorizeSchedule(tx, previous, input.caller, false);
|
|
71
|
+
if (input.op === "show") {
|
|
72
|
+
const runs = createJobDefinitionControl(tx, home)({
|
|
73
|
+
op: "runs", scope: input.scope, id: previous.jobId, caller: input.caller
|
|
74
|
+
}, now);
|
|
75
|
+
const ownRuns = runs.filter(r => r.trigger.source === "schedule" && r.trigger.key.startsWith(`${previous.id}:`));
|
|
76
|
+
const lastSuccess = ownRuns.filter(r => r.status === "succeeded")
|
|
77
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0] ?? null;
|
|
78
|
+
return { schedule: previous, lastSuccess };
|
|
79
|
+
}
|
|
80
|
+
if (previous.revision !== input.expectedRevision)
|
|
81
|
+
throw new Error("Schedule revision changed; read it before modifying.");
|
|
82
|
+
let next;
|
|
83
|
+
if (input.op === "disable") {
|
|
84
|
+
next = { ...previous, state: "disabled", reason: "Explicitly disabled; no future admission. Existing Runs are unchanged." };
|
|
85
|
+
}
|
|
86
|
+
else if (input.op === "enable" || input.op === "update") {
|
|
87
|
+
if (input.op === "enable" && ["completed", "retired"].includes(previous.state)) {
|
|
88
|
+
throw new Error("Completed/retired Schedule requires an explicit future rule update.");
|
|
89
|
+
}
|
|
90
|
+
const jobId = input.op === "update" ? input.jobId ?? previous.jobId : previous.jobId;
|
|
91
|
+
scheduleId(jobId);
|
|
92
|
+
const rule = input.op === "update" ? input.rule : previous.rule;
|
|
93
|
+
const job = authorizeSchedule(tx, { ...previous, jobId }, input.caller, true);
|
|
94
|
+
const after = fence(previous, now);
|
|
95
|
+
// Once overlap/disabled intent keeps its original due instant on enable.
|
|
96
|
+
const nextDue = input.op === "enable" && rule.kind === "once"
|
|
97
|
+
? previous.nextDue : nextScheduleDue(rule, after);
|
|
98
|
+
if (nextDue === null)
|
|
99
|
+
throw new Error("Rule must have a future occurrence.");
|
|
100
|
+
next = {
|
|
101
|
+
...previous, jobId, rule: structuredClone(rule), nextDue,
|
|
102
|
+
state: input.op === "enable" || previous.state === "enabled" ? "enabled" : "disabled",
|
|
103
|
+
generation: previous.generation + (input.op === "update" ? 1 : 0),
|
|
104
|
+
processedThrough: after.toISOString(),
|
|
105
|
+
authorization: { caller: structuredClone(input.caller), binding: binding(tx, input.caller, input.scope), jobRevision: job.revision }
|
|
106
|
+
};
|
|
107
|
+
const { reason: _reason, ...withoutReason } = next;
|
|
108
|
+
next = withoutReason;
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
throw new Error(`Unknown Schedule operation: ${input.op}.`);
|
|
112
|
+
next = { ...next, revision: previous.revision + 1, updatedAt: now.toISOString() };
|
|
113
|
+
tx.saveSchedule(next);
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** One bounded pass in the existing Controller. Transactions never spawn. */
|
|
119
|
+
export function reconcileSchedules(store, home, now, onError = () => { }, limit = 100) {
|
|
120
|
+
for (const candidate of store.listDueSchedules(now.toISOString(), limit)) {
|
|
121
|
+
try {
|
|
122
|
+
store.transaction(tx => {
|
|
123
|
+
const schedule = tx.getSchedule(candidate.scope, candidate.id);
|
|
124
|
+
if (schedule?.state !== "enabled" || schedule.nextDue === null || Date.parse(schedule.nextDue) > now.getTime())
|
|
125
|
+
return;
|
|
126
|
+
const dueAt = schedule.nextDue;
|
|
127
|
+
const after = fence(schedule, now);
|
|
128
|
+
if (schedule.scope.kind === "task") {
|
|
129
|
+
const task = tx.getTask(schedule.scope.taskId);
|
|
130
|
+
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
131
|
+
tx.saveSchedule({ ...schedule, state: "needs-attention", revision: schedule.revision + 1,
|
|
132
|
+
reason: "Task execution unavailable; pending intent retained.", updatedAt: now.toISOString() });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const job = authorizeSchedule(tx, schedule, schedule.authorization.caller, true);
|
|
137
|
+
if (job.revision !== schedule.authorization.jobRevision
|
|
138
|
+
|| binding(tx, schedule.authorization.caller, schedule.scope) !== schedule.authorization.binding) {
|
|
139
|
+
throw new Error("Schedule authority or Job revision changed; inspect and explicitly enable.");
|
|
140
|
+
}
|
|
141
|
+
const result = admitJobRun(tx, home, schedule.scope, schedule.jobId, schedule.authorization.caller, { source: "schedule", key: `${schedule.id}:${schedule.generation}:${dueAt}` }, now);
|
|
142
|
+
const overlap = result.reason === "overlap";
|
|
143
|
+
const nextDue = overlap && schedule.rule.kind === "once" ? dueAt : nextScheduleDue(schedule.rule, after);
|
|
144
|
+
tx.saveSchedule({
|
|
145
|
+
...schedule, nextDue, state: nextDue === null ? "completed" : "enabled",
|
|
146
|
+
processedThrough: after.toISOString(), revision: schedule.revision + 1,
|
|
147
|
+
lastAttempt: { at: now.toISOString(), dueAt, outcome: overlap ? "overlap" : "admitted", runId: result.run.id },
|
|
148
|
+
updatedAt: now.toISOString()
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const reason = redactLaunchText(error instanceof Error ? error.message : String(error));
|
|
154
|
+
// Admission/advancement rolled back together. Do not retry an unknown error.
|
|
155
|
+
store.transaction(tx => {
|
|
156
|
+
const current = tx.getSchedule(candidate.scope, candidate.id);
|
|
157
|
+
if (current?.state !== "enabled" || current.revision !== candidate.revision)
|
|
158
|
+
return;
|
|
159
|
+
tx.saveSchedule({ ...current, state: "needs-attention", reason, revision: current.revision + 1,
|
|
160
|
+
lastAttempt: { at: now.toISOString(), dueAt: current.nextDue, outcome: "blocked", reason },
|
|
161
|
+
updatedAt: now.toISOString() });
|
|
162
|
+
if (current.scope.kind === "task") {
|
|
163
|
+
const event = createTaskEvent(tx.nextEventId(current.scope.taskId), current.scope.taskId, "schedule.needs-attention", { scheduleId: current.id, reason }, now);
|
|
164
|
+
tx.saveEvent(current.scope.taskId, event);
|
|
165
|
+
enqueueSupervisorEvent(tx, event, "leader", "schedule-needs-attention", now);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
tx.saveGlobalRoleMessage(createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", `Yui-Summary: Schedule ${current.id} needs attention; no Run was admitted.\n`
|
|
169
|
+
+ `Reason: ${reason}\nRead: yui schedule show --global --schedule ${current.id}\n`
|
|
170
|
+
+ "Inspect the original intent before explicitly enabling; no new authority is implied.", "system", { type: "system" }, now, { inputControl: { action: "queue", requestId: `schedule-attention:${current.id}:${current.revision}` } }));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
onError(new Error(`Schedule ${candidate.id} needs attention: ${reason}`, { cause: error }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -315,11 +315,11 @@ function parseTarget(value) {
|
|
|
315
315
|
function parseRef(value, label) {
|
|
316
316
|
const ref = record(value, label);
|
|
317
317
|
const types = [
|
|
318
|
-
"task", "run", "work-item", "input", "session", "message", "event"
|
|
318
|
+
"task", "run", "work-item", "input", "session", "message", "event", "global-job-run"
|
|
319
319
|
];
|
|
320
320
|
if (!types.includes(ref.type))
|
|
321
321
|
throw new Error(`${label} type is invalid`);
|
|
322
|
-
if (ref.type === "task" || ref.type === "session") {
|
|
322
|
+
if (ref.type === "task" || ref.type === "session" || ref.type === "global-job-run") {
|
|
323
323
|
exact(ref, ["type", "id"], label);
|
|
324
324
|
return { type: ref.type, id: requireString(ref.id, `${label} id`) };
|
|
325
325
|
}
|
|
@@ -360,7 +360,7 @@ function copyTarget(target) {
|
|
|
360
360
|
}
|
|
361
361
|
export function copyRef(ref) {
|
|
362
362
|
if (!("taskId" in ref)) {
|
|
363
|
-
if (ref.type !== "task" && ref.type !== "session") {
|
|
363
|
+
if (ref.type !== "task" && ref.type !== "session" && ref.type !== "global-job-run") {
|
|
364
364
|
throw new Error(`entity reference taskId is required for ${ref.type}`);
|
|
365
365
|
}
|
|
366
366
|
return { type: ref.type, id: requireText(ref.id, "entity reference id") };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
|
|
2
2
|
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
3
|
+
import { canonicalPublicationUrl } from "../task/publicationToolObservation.js";
|
|
3
4
|
const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
|
|
4
5
|
const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
5
6
|
const MAX_FAILURE_DETAIL_CHARS = 4_000;
|
|
@@ -20,22 +21,24 @@ async function inspectGitHubPullRequest(run, input) {
|
|
|
20
21
|
if (!PULL_REQUEST_NUMBER.test(input.externalId)) {
|
|
21
22
|
throw new Error(`GitHub pull request id must be numeric: ${input.externalId}.`);
|
|
22
23
|
}
|
|
24
|
+
const hostname = input.externalUrl === undefined
|
|
25
|
+
? "github.com" : new URL(canonicalPublicationUrl(input.externalUrl)).host;
|
|
23
26
|
const result = await run("gh", [
|
|
24
27
|
"pr", "view", input.externalId,
|
|
25
|
-
"--repo", input.repository
|
|
26
|
-
"--json", "number,state,headRefOid,mergeCommit,mergedAt,url"
|
|
28
|
+
"--repo", `${hostname}/${input.repository}`,
|
|
29
|
+
"--json", "number,state,headRefOid,mergeCommit,mergedAt,url,baseRefName"
|
|
27
30
|
]);
|
|
28
31
|
if (result.code !== 0) {
|
|
29
32
|
if (result.code === 127) {
|
|
30
|
-
throw new Error("
|
|
33
|
+
throw new Error("The built-in GitHub adapter needs gh on PATH; alternatively record authorized tool evidence with task publication verify --observation.");
|
|
31
34
|
}
|
|
32
35
|
const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
|
|
33
36
|
throw new Error(`gh pr view failed for ${input.repository}#${input.externalId} `
|
|
34
37
|
+ `(exit ${result.code})${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
35
38
|
}
|
|
36
|
-
return parseGitHubPullRequest(result.stdout, input.repository, input.externalId);
|
|
39
|
+
return parseGitHubPullRequest(result.stdout, input.repository, input.externalId, hostname);
|
|
37
40
|
}
|
|
38
|
-
export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
41
|
+
export function parseGitHubPullRequest(stdout, repository, externalId, expectedHost = "github.com") {
|
|
39
42
|
let parsed;
|
|
40
43
|
try {
|
|
41
44
|
parsed = JSON.parse(stdout.trim());
|
|
@@ -60,6 +63,11 @@ export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
|
60
63
|
throw new Error("Merged GitHub PR did not expose a merge commit.");
|
|
61
64
|
}
|
|
62
65
|
const externalUrl = optionalText(record.url);
|
|
66
|
+
if (externalUrl === undefined
|
|
67
|
+
|| canonicalPublicationUrl(externalUrl, "github")
|
|
68
|
+
!== canonicalPublicationUrl(`https://${expectedHost}/${repository}/pull/${externalId}`, "github")) {
|
|
69
|
+
throw new Error("GitHub PR URL does not match the expected service, repository and PR.");
|
|
70
|
+
}
|
|
63
71
|
const mergedAt = optionalText(record.mergedAt);
|
|
64
72
|
return {
|
|
65
73
|
provider: "github",
|
|
@@ -67,6 +75,7 @@ export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
|
67
75
|
externalKind: "pull-request",
|
|
68
76
|
externalId,
|
|
69
77
|
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
78
|
+
...(optionalText(record.baseRefName) === undefined ? {} : { targetBranch: optionalText(record.baseRefName) }),
|
|
70
79
|
state,
|
|
71
80
|
headCommit,
|
|
72
81
|
...(remoteCommit === undefined ? {} : { remoteCommit }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
|
|
2
2
|
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
3
|
+
import { canonicalPublicationUrl } from "../task/publicationToolObservation.js";
|
|
3
4
|
const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
|
|
4
5
|
const GITLAB_REPOSITORY = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/u;
|
|
5
6
|
const MAX_FAILURE_DETAIL_CHARS = 4_000;
|
|
@@ -30,7 +31,7 @@ async function inspectGitLabMergeRequest(run, input) {
|
|
|
30
31
|
]);
|
|
31
32
|
if (result.code !== 0) {
|
|
32
33
|
if (result.code === 127) {
|
|
33
|
-
throw new Error("
|
|
34
|
+
throw new Error("The built-in GitLab adapter needs glab on PATH; alternatively record authorized tool evidence with task publication verify --observation.");
|
|
34
35
|
}
|
|
35
36
|
const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
|
|
36
37
|
throw new Error(`glab api failed for ${input.repository}!${input.externalId} `
|
|
@@ -64,8 +65,13 @@ export function parseGitLabMergeRequest(stdout, repository, externalId, expected
|
|
|
64
65
|
}
|
|
65
66
|
const externalUrl = optionalText(record.web_url);
|
|
66
67
|
assertGitLabReference(record.references, repository, externalId);
|
|
67
|
-
if (externalUrl !== undefined
|
|
68
|
-
|
|
68
|
+
if (externalUrl !== undefined) {
|
|
69
|
+
const url = new URL(canonicalPublicationUrl(externalUrl));
|
|
70
|
+
// references.full and iid establish identity; web_url is a browser link,
|
|
71
|
+
// not a repository/MR path schema. The command also checks the candidate URL.
|
|
72
|
+
if (expectedHost !== undefined && url.host.toLowerCase() !== expectedHost.toLowerCase()) {
|
|
73
|
+
throw new Error(`GitLab MR web URL does not match service ${expectedHost}.`);
|
|
74
|
+
}
|
|
69
75
|
}
|
|
70
76
|
const mergedAt = optionalText(record.merged_at);
|
|
71
77
|
return {
|
|
@@ -74,6 +80,7 @@ export function parseGitLabMergeRequest(stdout, repository, externalId, expected
|
|
|
74
80
|
externalKind: "merge-request",
|
|
75
81
|
externalId,
|
|
76
82
|
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
83
|
+
...(optionalText(record.target_branch) === undefined ? {} : { targetBranch: optionalText(record.target_branch) }),
|
|
77
84
|
state,
|
|
78
85
|
headCommit,
|
|
79
86
|
...(remoteCommit === undefined ? {} : { remoteCommit }),
|
|
@@ -102,21 +109,6 @@ function assertGitLabReference(value, repository, externalId) {
|
|
|
102
109
|
+ `${repository}!${externalId}.`);
|
|
103
110
|
}
|
|
104
111
|
}
|
|
105
|
-
function assertGitLabWebUrl(value, expectedHost, repository, externalId) {
|
|
106
|
-
let url;
|
|
107
|
-
try {
|
|
108
|
-
url = new URL(value);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
throw new Error("GitLab MR web URL is invalid.");
|
|
112
|
-
}
|
|
113
|
-
const expectedSuffix = `/${repository}/-/merge_requests/${externalId}`;
|
|
114
|
-
if ((url.protocol !== "https:" && url.protocol !== "http:")
|
|
115
|
-
|| url.host.toLowerCase() !== expectedHost.toLowerCase()
|
|
116
|
-
|| !url.pathname.endsWith(expectedSuffix)) {
|
|
117
|
-
throw new Error(`GitLab MR web URL does not match ${expectedHost}/${repository}!${externalId}.`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
112
|
function firstCommit(...values) {
|
|
121
113
|
for (const value of values) {
|
|
122
114
|
if (value === null || value === undefined || value === "")
|
|
@@ -139,17 +131,8 @@ function optionalText(value) {
|
|
|
139
131
|
function gitLabHostname(externalUrl) {
|
|
140
132
|
if (externalUrl === undefined)
|
|
141
133
|
return undefined;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
url = new URL(externalUrl);
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
throw new Error(`GitLab external URL is invalid: ${externalUrl}.`);
|
|
148
|
-
}
|
|
149
|
-
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
150
|
-
throw new Error(`GitLab external URL protocol is invalid: ${url.protocol}.`);
|
|
151
|
-
}
|
|
152
|
-
return url.hostname === "gitlab.com" ? undefined : url.host;
|
|
134
|
+
const url = new URL(canonicalPublicationUrl(externalUrl));
|
|
135
|
+
return url.host === "gitlab.com" ? undefined : url.host;
|
|
153
136
|
}
|
|
154
137
|
function boundedFailureDetail(value) {
|
|
155
138
|
return value.length <= MAX_FAILURE_DETAIL_CHARS
|
|
@@ -13,6 +13,12 @@ export function taskAuthorizationSource(store, taskId, environment, messageId, p
|
|
|
13
13
|
|| task.executionGate?.state === "stopped") {
|
|
14
14
|
throw usageError("Task authorization is unavailable in this lifecycle or execution gate.");
|
|
15
15
|
}
|
|
16
|
+
return { ...readTaskAuthorizationMessage(store, taskId, messageId, purpose),
|
|
17
|
+
nativeSessionId: caller.nativeSessionId };
|
|
18
|
+
}
|
|
19
|
+
/** Shared source binding; interpreting the quoted authority remains the caller's
|
|
20
|
+
* responsibility. A matching quote proves provenance, not semantic permission. */
|
|
21
|
+
export function readTaskAuthorizationMessage(store, taskId, messageId, purpose) {
|
|
16
22
|
const message = store.listMessages(taskId).find(entry => entry.id === messageId);
|
|
17
23
|
if (!message || !((message.kind === "user" && message.author.type === "user")
|
|
18
24
|
|| (message.kind === "operator" && message.author.type === "operator"))) {
|
|
@@ -29,8 +35,7 @@ export function taskAuthorizationSource(store, taskId, environment, messageId, p
|
|
|
29
35
|
taskId: message.taskId, id: message.id, kind: message.kind,
|
|
30
36
|
author: message.author, body: message.body, createdAt: message.createdAt
|
|
31
37
|
})).digest("hex"),
|
|
32
|
-
purpose: purpose.trim()
|
|
33
|
-
nativeSessionId: caller.nativeSessionId
|
|
38
|
+
purpose: purpose.trim()
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
41
|
const RELEASE_ACTIONS = new Set([
|
|
@@ -49,11 +49,11 @@ export function wakeChangeLabels(reasons) {
|
|
|
49
49
|
}
|
|
50
50
|
}))];
|
|
51
51
|
}
|
|
52
|
-
export function wakeMessages(wake, messages) {
|
|
52
|
+
export function wakeMessages(wake, messages, history = false) {
|
|
53
53
|
const ids = new Set(wake.refs?.filter(ref => ref.type === "message" && ref.taskId === wake.taskId).map(ref => ref.id));
|
|
54
54
|
const from = Date.parse(wake.fromCursor), to = Date.parse(wake.toCursor);
|
|
55
|
-
return messages.filter(message => ids.has(message.id)
|
|
56
|
-
|| Date.parse(message.createdAt) > from && Date.parse(message.createdAt) <= to);
|
|
55
|
+
return messages.filter(message => message.taskId === wake.taskId && (ids.has(message.id)
|
|
56
|
+
|| (history || wake.refs === undefined) && Date.parse(message.createdAt) > from && Date.parse(message.createdAt) <= to));
|
|
57
57
|
}
|
|
58
58
|
export function taskNotificationPresentation(wake, store) {
|
|
59
59
|
const messages = wakeMessages(wake, store.listMessages(wake.taskId));
|
|
@@ -69,6 +69,13 @@ export function taskNotificationPresentation(wake, store) {
|
|
|
69
69
|
...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
|
|
70
70
|
`Refresh: yui task context ${wake.taskId} --json`,
|
|
71
71
|
`Read inputs/results: yui task wake show ${wake.taskId} ${wake.id} --json`,
|
|
72
|
+
`Handling: claim exact inbox entry ${wake.id}; after disposition, ack it with a concrete reason before ending this turn.`,
|
|
73
|
+
"Use yui inbox list --view current --json for current inputs. Historical missing receipts are not new work; never replay effects to repair an ACK.",
|
|
74
|
+
...(wake.reasons.includes("session-replaced") ? [
|
|
75
|
+
"Session replacement is initialization, not replay or completion of earlier input.",
|
|
76
|
+
"Read yui inbox list --view history --json; inspect retained originals, prior effects and current authority before explicit settlement.",
|
|
77
|
+
"Preserve original claims and delivery targets. Unknown external effects require evidence, not another execution."
|
|
78
|
+
] : []),
|
|
72
79
|
"Check current Skills each turn: yui session skills --json. Apply current yui-leader; read relevant originals in full and advance authorized work."
|
|
73
80
|
].join("\n");
|
|
74
81
|
}
|
|
@@ -80,6 +87,8 @@ export function globalMessagePresentation(roleName, messageId, message) {
|
|
|
80
87
|
...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
|
|
81
88
|
"Refresh Session Context using the current Manifest's loadCommand.",
|
|
82
89
|
`Read original: yui role message show ${roleName} ${messageId} --json`,
|
|
90
|
+
`Handling: claim exact inbox entry ${messageId}; after disposition, ack it with a concrete reason before ending this turn.`,
|
|
91
|
+
"Routing to a Task or asking a necessary user question can settle this input without completing the Task. Missing ACK is not permission to repeat effects.",
|
|
83
92
|
`The Global loadCommand checks current Skill versions each turn. Apply current ${roleName === "operator" ? "yui-operator" : "yui-runtime"} within existing authority.`
|
|
84
93
|
].join("\n");
|
|
85
94
|
}
|
|
@@ -16,6 +16,11 @@ export function createOperatorBatchPresentation(batchId, items) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
function renderItem(item) {
|
|
19
|
+
if (item.kind === "global-job-run")
|
|
20
|
+
return [
|
|
21
|
+
`- Global Job Run ${item.run.status}: ${item.run.definitionId}/${item.run.id}`,
|
|
22
|
+
` Inspect: yui job run-get --global --job ${item.run.definitionId} --run ${item.run.id}`
|
|
23
|
+
];
|
|
19
24
|
if (item.kind === "input-request") {
|
|
20
25
|
const { request } = item;
|
|
21
26
|
return [
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { requireKnownFields } from "../domain/validation.js";
|
|
3
|
+
/**
|
|
4
|
+
* Controlled action compilation, not a shell/Agent plugin surface.
|
|
5
|
+
* Unknown actions fail closed; executable paths/argv are never caller supplied.
|
|
6
|
+
*/
|
|
7
|
+
export function prepareBuiltinJob(configuration, workspace, execution) {
|
|
8
|
+
if (configuration.action === "project.refresh") {
|
|
9
|
+
projectRefreshDigest(configuration);
|
|
10
|
+
// Runtime-owned Git discovery/authentication context, never caller input
|
|
11
|
+
// or arbitrary inherited credentials. Background Git cannot prompt.
|
|
12
|
+
const env = { GIT_TERMINAL_PROMPT: "0" };
|
|
13
|
+
for (const key of ["PATH", "HOME", "SSH_AUTH_SOCK", "XDG_CONFIG_HOME", "GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_GLOBAL"]) {
|
|
14
|
+
if (process.env[key] !== undefined)
|
|
15
|
+
env[key] = process.env[key];
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
workspace, env, head: "0".repeat(40),
|
|
19
|
+
steps: [{
|
|
20
|
+
name: "project.refresh", command: `project.refresh ${configuration.projectId}`,
|
|
21
|
+
argv: [process.execPath, fileURLToPath(new URL("./projectRefreshRunner.js", import.meta.url)),
|
|
22
|
+
execution?.home ?? "", execution?.runId ?? ""],
|
|
23
|
+
timeoutMs: 10 * 60_000
|
|
24
|
+
}]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (configuration.action !== "system.noop")
|
|
28
|
+
throw new Error(`Unsupported builtin Job action: ${configuration.action}.`);
|
|
29
|
+
if (configuration.projectId !== undefined || JSON.stringify(configuration.input) !== "{}") {
|
|
30
|
+
throw new Error("system.noop accepts an empty input and no Project target.");
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
workspace,
|
|
34
|
+
env: {},
|
|
35
|
+
// No source repository applies to this action. This explicit sentinel is
|
|
36
|
+
// never an Integration candidate or evidence of a Project revision.
|
|
37
|
+
head: "0".repeat(40),
|
|
38
|
+
steps: [{
|
|
39
|
+
name: "system.noop",
|
|
40
|
+
command: "system.noop",
|
|
41
|
+
argv: [process.execPath, "-e", "process.stdout.write('system.noop completed\\n')"],
|
|
42
|
+
timeoutMs: 5000
|
|
43
|
+
}]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function projectRefreshDigest(configuration) {
|
|
47
|
+
if (!/^project-\d+$/u.test(configuration.projectId ?? ""))
|
|
48
|
+
throw new Error("project.refresh requires an exact Project ID.");
|
|
49
|
+
const input = configuration.input;
|
|
50
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
51
|
+
throw new Error("Invalid project.refresh input.");
|
|
52
|
+
requireKnownFields(input, ["targetDigest"], "project.refresh input");
|
|
53
|
+
const digest = input.targetDigest;
|
|
54
|
+
if (typeof digest !== "string" || !/^[a-f0-9]{64}$/u.test(digest)) {
|
|
55
|
+
throw new Error("project.refresh requires its current catalog targetDigest.");
|
|
56
|
+
}
|
|
57
|
+
return digest;
|
|
58
|
+
}
|