@zq-silk/yui 0.15.11 → 0.15.12
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/ARCHITECTURE.md +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +38 -10
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli.js +46 -15
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +27 -2
- package/dist/commands/projectCommands.js +44 -15
- package/dist/commands/taskCommands.js +95 -33
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +17 -4
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +36 -1
- package/dist/integration/gitIntegrationService.js +19 -6
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +71 -124
- package/dist/repository/taskWorkspacePreparer.js +86 -24
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +28 -11
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/taskExecutionProjection.js +26 -5
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/storage/sqliteSchema.js +31 -0
- package/dist/storage/sqliteStore.js +17 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +2 -1
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +14 -3
- package/dist/web/webSnapshot.js +12 -3
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +16 -0
- package/docs/testing/verification-levels.zh-CN.md +13 -1
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-operator/SKILL.md +13 -2
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Current facts, not another persisted projection or execution status. */
|
|
2
|
+
export const CATALOG_ATTENTION = [
|
|
3
|
+
"openInputs", "pendingOperations", "unknownOperations", "executionSignals"
|
|
4
|
+
];
|
|
5
|
+
/**
|
|
6
|
+
* All filtering/counting stays in SQLite. Only page-sized rows and fixed
|
|
7
|
+
* attention samples cross into JS; no Task/Run/Message/Event body is decoded.
|
|
8
|
+
* The existing catalog and record tables remain the only stored authorities.
|
|
9
|
+
*/
|
|
10
|
+
export function queryTaskCatalog(db, query) {
|
|
11
|
+
const scope = ["1=1"];
|
|
12
|
+
const params = { limit: query.limit };
|
|
13
|
+
if (query.taskId !== undefined) {
|
|
14
|
+
scope.push("c.task_id = @taskId");
|
|
15
|
+
params.taskId = query.taskId;
|
|
16
|
+
}
|
|
17
|
+
if (!query.all)
|
|
18
|
+
scope.push("c.status <> 'archived'");
|
|
19
|
+
const match = ["1=1"];
|
|
20
|
+
if (query.status !== undefined) {
|
|
21
|
+
match.push("status = @status");
|
|
22
|
+
params.status = query.status;
|
|
23
|
+
}
|
|
24
|
+
if (query.project !== undefined) {
|
|
25
|
+
match.push(`EXISTS (SELECT 1 FROM task_records r, json_each(r.payload, '$.projectBindings') p
|
|
26
|
+
WHERE r.task_id = facts.id AND json_extract(p.value, '$.projectId') = @project)`);
|
|
27
|
+
params.project = query.project;
|
|
28
|
+
}
|
|
29
|
+
if (query.search) {
|
|
30
|
+
match.push(`EXISTS (SELECT 1 FROM task_records r WHERE r.task_id = facts.id AND
|
|
31
|
+
(instr(lower(json_extract(r.payload, '$.title')), lower(@search)) > 0
|
|
32
|
+
OR instr(lower(r.task_id), lower(@search)) > 0
|
|
33
|
+
OR EXISTS (SELECT 1 FROM json_each(r.payload, '$.tags') tag
|
|
34
|
+
WHERE instr(lower(tag.value), lower(@search)) > 0)
|
|
35
|
+
OR EXISTS (SELECT 1 FROM json_each(r.payload, '$.projectBindings') b
|
|
36
|
+
JOIN projects p ON p.id = json_extract(b.value, '$.projectId')
|
|
37
|
+
WHERE instr(lower(json_extract(p.payload, '$.name')), lower(@search)) > 0)))`);
|
|
38
|
+
params.search = query.search;
|
|
39
|
+
}
|
|
40
|
+
if (query.attention !== undefined)
|
|
41
|
+
match.push(`${query.attention} > 0`);
|
|
42
|
+
const position = (name, op) => {
|
|
43
|
+
const value = query[name];
|
|
44
|
+
if (value === undefined)
|
|
45
|
+
return;
|
|
46
|
+
params[`${name}Date`] = value.createdAt;
|
|
47
|
+
params[`${name}Id`] = value.id;
|
|
48
|
+
match.push(`(createdAt, id) ${op} (@${name}Date, @${name}Id)`);
|
|
49
|
+
};
|
|
50
|
+
position("through", "<=");
|
|
51
|
+
// A fixed upper key excludes newly-created Tasks on later pages. Updates
|
|
52
|
+
// to mutable filters are current reads, not a snapshot/session protocol.
|
|
53
|
+
const after = [];
|
|
54
|
+
if (query.after !== undefined) {
|
|
55
|
+
params.afterDate = query.after.createdAt;
|
|
56
|
+
params.afterId = query.after.id;
|
|
57
|
+
after.push("(createdAt, id) > (@afterDate, @afterId)");
|
|
58
|
+
}
|
|
59
|
+
const count = (table, condition) => `(SELECT count(*) FROM ${table} x WHERE x.task_id = c.task_id AND (${condition}))`;
|
|
60
|
+
// Execution signals deliberately preserve raw inspectable conditions:
|
|
61
|
+
// live runs (including unknown admission/identity/stall), unresolved work,
|
|
62
|
+
// integrations, and pending Leader recovery. They are NOT a second
|
|
63
|
+
// classifier for working/blocked/success; precise execution stays in detail.
|
|
64
|
+
// Including all live runs is conservative: an off-page runtime attention
|
|
65
|
+
// cannot disappear just because a lightweight read did not fold its history.
|
|
66
|
+
const cte = `WITH facts AS MATERIALIZED (
|
|
67
|
+
SELECT c.task_id AS id, c.created_at AS createdAt, c.updated_at AS updatedAt, c.status,
|
|
68
|
+
${count("work_items", "1=1")} AS workItems,
|
|
69
|
+
${count("turns", "status = 'active'")} AS activeRuns,
|
|
70
|
+
${count("input_requests", "status = 'open'")} AS openInputs,
|
|
71
|
+
${count("durable_jobs", "status IN ('queued','running')")} AS pendingOperations,
|
|
72
|
+
${count("durable_jobs", "status = 'unknown-needs-attention'")} AS unknownOperations,
|
|
73
|
+
CASE WHEN c.status IN ('active','draft') THEN
|
|
74
|
+
${count("turns", "status = 'active'")} +
|
|
75
|
+
${count("work_items", "status = 'open' OR (status = 'accepted' AND json_extract(payload, '$.currentExecutionGroupId') IS NOT NULL)")} +
|
|
76
|
+
${count("integration_attempts", "status IN ('running','validating','blocked','conflicted','failed')")} +
|
|
77
|
+
${count("review_rounds", "status IN ('pending','running','failed')")} +
|
|
78
|
+
${count("task_projections", "kind = 'leader-failure' AND payload IS NOT NULL AND payload <> 'null'")} +
|
|
79
|
+
${count("mailboxes", "role_name = 'leader' AND (json_type(pending) = 'object' OR json_type(processing) = 'object')")}
|
|
80
|
+
ELSE 0 END AS executionSignals
|
|
81
|
+
FROM tasks_catalog c WHERE ${scope.join(" AND ")}
|
|
82
|
+
), matching AS MATERIALIZED (SELECT * FROM facts WHERE ${match.join(" AND ")})
|
|
83
|
+
SELECT
|
|
84
|
+
(SELECT json_group_array(json_object('status',status,'count',n))
|
|
85
|
+
FROM (SELECT status,count(*) n FROM facts GROUP BY status)) AS counts,
|
|
86
|
+
(SELECT count(*) FROM matching) AS total,
|
|
87
|
+
(SELECT json_object('id',id,'createdAt',createdAt) FROM matching
|
|
88
|
+
ORDER BY createdAt DESC,id DESC LIMIT 1) AS upper,
|
|
89
|
+
${CATALOG_ATTENTION.map(kind => `
|
|
90
|
+
(SELECT sum(${kind}) FROM facts) AS ${kind}Count,
|
|
91
|
+
(SELECT count(*) FROM facts WHERE ${kind} > 0) AS ${kind}Tasks,
|
|
92
|
+
(SELECT json_group_array(id) FROM (SELECT id FROM facts WHERE ${kind} > 0
|
|
93
|
+
ORDER BY createdAt,id LIMIT 4)) AS ${kind}Ids`).join(",")},
|
|
94
|
+
(SELECT json_group_array(json_object(
|
|
95
|
+
'id',page.id,'createdAt',page.createdAt,'updatedAt',page.updatedAt,'status',page.status,
|
|
96
|
+
'title',substr(json_extract(r.payload,'$.title'),1,256),
|
|
97
|
+
'summary',substr(json_extract(r.brief,'$.leaderSummary'),1,512),
|
|
98
|
+
'summaryPresent',json(CASE WHEN r.brief IS NULL THEN 'false' ELSE 'true' END),
|
|
99
|
+
'workItems',page.workItems,'activeRuns',page.activeRuns,
|
|
100
|
+
'openInputs',page.openInputs,'pendingOperations',page.pendingOperations,
|
|
101
|
+
'unknownOperations',page.unknownOperations,'executionSignals',page.executionSignals)
|
|
102
|
+
ORDER BY page.createdAt,page.id)
|
|
103
|
+
FROM (SELECT * FROM matching ${after.length ? `WHERE ${after.join(" AND ")}` : ""}
|
|
104
|
+
ORDER BY createdAt,id LIMIT @limit) page
|
|
105
|
+
JOIN task_records r ON r.task_id = page.id) AS rows`;
|
|
106
|
+
const result = db.prepare(cte).get(params);
|
|
107
|
+
const counts = { draft: 0, active: 0, completed: 0, cancelled: 0, archived: 0, total: 0 };
|
|
108
|
+
for (const entry of JSON.parse(result.counts)) {
|
|
109
|
+
counts[entry.status] = entry.count;
|
|
110
|
+
counts.total += entry.count;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
rows: JSON.parse(result.rows),
|
|
114
|
+
total: result.total,
|
|
115
|
+
through: result.upper === null ? null : JSON.parse(result.upper),
|
|
116
|
+
counts,
|
|
117
|
+
attention: Object.fromEntries(CATALOG_ATTENTION.map(kind => [kind, {
|
|
118
|
+
count: Number(result[`${kind}Count`] ?? 0),
|
|
119
|
+
taskCount: Number(result[`${kind}Tasks`] ?? 0),
|
|
120
|
+
taskIds: JSON.parse(result[`${kind}Ids`])
|
|
121
|
+
}]))
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -448,6 +448,8 @@ export function storedPublicationReference(value) {
|
|
|
448
448
|
fields.push("targetBranch");
|
|
449
449
|
if (reference.localCommit !== undefined)
|
|
450
450
|
fields.push("localCommit");
|
|
451
|
+
if (reference.headCommit !== undefined)
|
|
452
|
+
fields.push("headCommit");
|
|
451
453
|
if (reference.remoteCommit !== undefined)
|
|
452
454
|
fields.push("remoteCommit");
|
|
453
455
|
if (reference.evidence !== undefined)
|
|
@@ -122,6 +122,7 @@ export function taskArchiveDiagnostics(store, task) {
|
|
|
122
122
|
export function renderArchiveDiagnostics(data) {
|
|
123
123
|
return [
|
|
124
124
|
`Archived: ${data.archived}; forced: ${data.forced}; cleanup finished: ${data.cleanupFinished}`,
|
|
125
|
+
`Retained references: ${data.retainedResources.length}; a finished cleanup attempt is not physical resource release evidence.`,
|
|
125
126
|
...data.warnings.map(w => `Warning [${w.resource}]: ${w.detail}`),
|
|
126
127
|
...data.retainedResources.map(r => `Retained [${r.resource}]: ${r.detail}${r.paths ? ` (${r.paths.join(", ")})` : ""}`)
|
|
127
128
|
].join("\n") + "\n";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readArchiveDelivery } from "../repository/workspaceCleanupInspection.js";
|
|
2
|
+
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
3
|
+
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
4
|
+
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
5
|
+
import { cleanupCheckFromError, renderCleanupCheck } from "../workspace/cleanupInspection.js";
|
|
6
|
+
/** Semantic admission facts. Cleanup still has its own resource boundary. */
|
|
7
|
+
export function archiveSettlementChecks(store, task) {
|
|
8
|
+
const checks = [];
|
|
9
|
+
const add = (resource, reason, detail, expected, observed, action) => checks.push({ resource, reason, status: "blocked", detail, expected, observed, sources: [resource], actions: [action] });
|
|
10
|
+
for (const input of store.listInputRequests(task.id).filter(i => i.status === "open")) {
|
|
11
|
+
add(`input:${task.id}/${input.id}`, "unresolved-input", "Open user input is not answered by archive.", "resolved input", input.status, `yui task input show ${task.id}/${input.id}`);
|
|
12
|
+
}
|
|
13
|
+
for (const item of store.listWorkItems(task.id).filter(i => i.status === "open")) {
|
|
14
|
+
add(`work-item:${task.id}/${item.id}`, "owner-unsettled", "WorkItem must be accepted or explicitly retired before archive.", ["accepted", "retired"], item.status, `yui task work show ${task.id}/${item.id}`);
|
|
15
|
+
}
|
|
16
|
+
for (const attempt of store.listIntegrationAttempts(task.id).filter(a => ["running", "blocked", "conflicted", "validating"].includes(a.status))) {
|
|
17
|
+
add(`integration-attempt:${task.id}/${attempt.id}`, "unresolved-integration", "Task has an unresolved Integration Attempt.", "terminal attempt", attempt.status, `yui task integration show ${task.id}/${attempt.id}`);
|
|
18
|
+
}
|
|
19
|
+
for (const job of store.listDurableJobs(task.id).filter(j => ["queued", "running"].includes(j.status) || (j.status === "unknown-needs-attention" && j.acknowledgedAt === undefined))) {
|
|
20
|
+
add(`job:${task.id}/${job.id}`, "active-durable-job", "Task has an active or unacknowledged DurableJob.", "settled job", job.status, `yui job get --task ${task.id} --job ${job.id}`);
|
|
21
|
+
}
|
|
22
|
+
return checks;
|
|
23
|
+
}
|
|
24
|
+
/** Unknown execution is never permission to remove workspaces, even after
|
|
25
|
+
* force admission. These facts are reloaded in the actual force cleanup path.
|
|
26
|
+
*/
|
|
27
|
+
export function archiveExecutionChecks(store, taskId) {
|
|
28
|
+
const checks = [];
|
|
29
|
+
const add = (resource, reason, detail, observed, action) => checks.push({ resource, reason, status: "unknown", detail, expected: "settled execution",
|
|
30
|
+
observed, sources: [resource], actions: [action] });
|
|
31
|
+
for (const run of store.listRuns(taskId).filter(r => r.status === "active")) {
|
|
32
|
+
add(`run:${taskId}/${run.id}`, "active-turn", "AgentRun remains active; archive is not stop evidence.", run.status, `yui task run show ${taskId}/${run.id}`);
|
|
33
|
+
}
|
|
34
|
+
for (const job of store.listDurableJobs(taskId).filter(j => ["queued", "running", "unknown-needs-attention"].includes(j.status))) {
|
|
35
|
+
add(`job:${taskId}/${job.id}`, "unresolved-execution", "DurableJob may still hold resources; acknowledgement is not physical exit evidence.", job.status, `yui job get --task ${taskId} --job ${job.id}`);
|
|
36
|
+
}
|
|
37
|
+
for (const role of store.listRoles(taskId)) {
|
|
38
|
+
const resource = `role:${taskId}/${role.name}`;
|
|
39
|
+
const action = `yui task role session inspect ${taskId} ${role.name}`;
|
|
40
|
+
const provider = store.getTaskRoleSessionSet(taskId, role.name)?.providerBinding;
|
|
41
|
+
const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: role.name });
|
|
42
|
+
if (provider?.run != null && !["completed", "failed", "cancelled"].includes(provider.run.status)) {
|
|
43
|
+
add(resource, "unresolved-provider-input", "Original provider input outcome is unresolved; do not replay it.", { attemptId: provider.run.attemptId, status: provider.run.status }, action);
|
|
44
|
+
}
|
|
45
|
+
if (mailbox?.processing != null && (provider?.run == null
|
|
46
|
+
|| !["completed", "failed", "cancelled"].includes(provider.run.status))) {
|
|
47
|
+
add(resource, "unresolved-mailbox", "Original mailbox claim has no proven terminal; archive does not acknowledge it.", { batchId: mailbox.processing.batchId }, action);
|
|
48
|
+
}
|
|
49
|
+
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "task", taskId, roleName: role.name })))) {
|
|
50
|
+
add(resource, "unresolved-runtime-lifecycle", "Role has unsettled runtime lifecycle work.", "pending", action);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return checks;
|
|
54
|
+
}
|
|
55
|
+
export async function inspectTaskArchive(coordinator, request) {
|
|
56
|
+
const { store, preparer, runtime } = coordinator;
|
|
57
|
+
const task = store.getTask(request.taskId);
|
|
58
|
+
if (task === null)
|
|
59
|
+
throw new Error(`Task not found: ${request.taskId}.`);
|
|
60
|
+
const settlement = archiveSettlementChecks(store, task);
|
|
61
|
+
const delivery = await readArchiveDelivery(store, preparer.git, task, request.force);
|
|
62
|
+
const deliveryChecks = delivery.projects.filter(p => p.codeDelivery !== "none" && (!p.merged || !p.verified))
|
|
63
|
+
.map(p => ({ resource: `delivery:${task.id}/${p.projectId}`, reason: "delivery-coverage", status: "blocked",
|
|
64
|
+
detail: p.reason, expected: { merged: true, verified: true, localCommit: p.expectedLocalCommit },
|
|
65
|
+
observed: { coverage: p.coverage, merged: p.merged, verified: p.verified, localCommit: p.publication?.localCommit ?? null },
|
|
66
|
+
sources: [`task:${task.id}`, ...(p.publication === null ? [] : [`publication:${task.id}/${p.publication.id}`])],
|
|
67
|
+
actions: [`yui task remote-delivery ${task.id}`] }));
|
|
68
|
+
const execution = archiveExecutionChecks(store, task.id);
|
|
69
|
+
const runtimeResource = `runtime:${task.id}`;
|
|
70
|
+
if (runtime.assertTaskPhysicalResourcesReleased === undefined) {
|
|
71
|
+
execution.push(...cleanupCheckFromError(null, runtimeResource, [`task:${task.id}`], [`yui task role status ${task.id}`]));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
try {
|
|
75
|
+
await runtime.assertTaskPhysicalResourcesReleased(task.id);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
execution.push({ resource: runtimeResource, reason: "physical-resources-unreleased", status: "unknown",
|
|
79
|
+
detail: error instanceof WorkspaceCleanupBlockedError ? error.message
|
|
80
|
+
: "Exact physical resource release is not established by the runtime inspection.",
|
|
81
|
+
expected: "proven physical absence",
|
|
82
|
+
observed: error instanceof WorkspaceCleanupBlockedError ? { reason: error.reason, resource: error.resource } : "unavailable",
|
|
83
|
+
sources: [`task:${task.id}`],
|
|
84
|
+
actions: [`yui task role status ${task.id}`, `yui session reconcile --report`] });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const resources = [];
|
|
88
|
+
for (const workspace of store.listManagedWorkspaces(task.id)) {
|
|
89
|
+
const disposition = workspace.owner.type === "work-item"
|
|
90
|
+
&& store.getWorkItem(task.id, workspace.owner.workItemId)?.status === "retired" ? "abandoned" : request.disposition;
|
|
91
|
+
let checks;
|
|
92
|
+
try {
|
|
93
|
+
checks = await preparer.inspectWorkspaceCleanup(workspace, disposition, request.force);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
checks = cleanupCheckFromError(error, managedWorkspaceKey(workspace.owner), [managedWorkspaceKey(workspace.owner)], [`yui task show ${task.id}`]);
|
|
97
|
+
}
|
|
98
|
+
resources.push({ resource: managedWorkspaceKey(workspace.owner), owner: workspace.owner,
|
|
99
|
+
status: checks.some(c => c.status === "unknown") ? "unknown" : checks.length > 0 ? "blocked" : "checked",
|
|
100
|
+
checks });
|
|
101
|
+
}
|
|
102
|
+
const eligible = ["completed", "cancelled", "archived"].includes(task.status);
|
|
103
|
+
return { taskId: task.id, observedAt: new Date().toISOString(), disposition: request.disposition, force: request.force,
|
|
104
|
+
readOnly: true, authorizesCleanup: false,
|
|
105
|
+
archive: { status: task.status, eligible, alreadyArchived: task.status === "archived",
|
|
106
|
+
settlement, delivery: deliveryChecks, forceBypassesSettlement: request.force,
|
|
107
|
+
requiresIndependentAuthorization: true },
|
|
108
|
+
cleanup: { execution, resources },
|
|
109
|
+
note: "Current observations only. Execution reloads the checks. Force commits archive first and retains unsafe resources; a finished attempt does not prove resource release." };
|
|
110
|
+
}
|
|
111
|
+
export function renderTaskArchivePreflight(data) {
|
|
112
|
+
return [
|
|
113
|
+
`Archive preflight: ${data.taskId} (${data.disposition}${data.force ? ", force" : ""}); read-only, not authorization`,
|
|
114
|
+
`Task: ${data.archive.status}; terminal eligibility: ${data.archive.eligible}`,
|
|
115
|
+
...data.archive.settlement.map(c => `Admission${data.force ? " (force preserves)" : ""}: ${renderCleanupCheck(c)}`),
|
|
116
|
+
...data.archive.delivery.map(c => `Delivery${data.force || data.disposition === "abandoned" ? " (advisory for admission)" : ""}: ${renderCleanupCheck(c)}`),
|
|
117
|
+
...data.cleanup.execution.map(c => `Cleanup: ${renderCleanupCheck(c)}`),
|
|
118
|
+
...data.cleanup.resources.flatMap(r => [
|
|
119
|
+
`Workspace [${r.resource}]: ${r.status}`,
|
|
120
|
+
...r.checks.map(c => ` ${renderCleanupCheck(c)}`)
|
|
121
|
+
]),
|
|
122
|
+
data.note
|
|
123
|
+
].join("\n") + "\n";
|
|
124
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { contextContentDigest } from "../context/contextSnapshot.js";
|
|
2
|
+
import { publicationExternalKey } from "./publicationReference.js";
|
|
3
|
+
export const PUBLICATION_ADOPTED_EVENT = "publication.candidate-adopted";
|
|
4
|
+
/**
|
|
5
|
+
* An explicit semantic decision over fixed Git endpoints, not an ancestry
|
|
6
|
+
* heuristic or a replacement completion head. Publication metadata/verification
|
|
7
|
+
* successors may retain it only while every intervening local candidate agrees.
|
|
8
|
+
*/
|
|
9
|
+
export function publicationAdoption(publication, completion, acceptedCommit, events, publications, integrations) {
|
|
10
|
+
const unavailable = { event: null, reason: null };
|
|
11
|
+
if (completion === undefined || acceptedCommit === null)
|
|
12
|
+
return unavailable;
|
|
13
|
+
const lineage = new Set();
|
|
14
|
+
let current = publication;
|
|
15
|
+
while (current !== undefined && !lineage.has(current.id)
|
|
16
|
+
&& current.taskId === publication.taskId
|
|
17
|
+
&& current.projectId === publication.projectId
|
|
18
|
+
&& current.externalKind === publication.externalKind
|
|
19
|
+
&& publicationExternalKey(current) === publicationExternalKey(publication)
|
|
20
|
+
&& current.localCommit === publication.localCommit) {
|
|
21
|
+
lineage.add(current.id);
|
|
22
|
+
current = publications.find(p => p.id === current.supersedes);
|
|
23
|
+
}
|
|
24
|
+
const event = events.slice().reverse().find(event => {
|
|
25
|
+
const p = event.payload;
|
|
26
|
+
return event.type === PUBLICATION_ADOPTED_EVENT
|
|
27
|
+
&& event.taskId === publication.taskId
|
|
28
|
+
&& p.completionEventId === completion.id
|
|
29
|
+
&& p.acceptedCommit === acceptedCommit
|
|
30
|
+
&& p.projectId === publication.projectId
|
|
31
|
+
&& p.localCommit === publication.localCommit
|
|
32
|
+
&& lineage.has(p.publicationId ?? "");
|
|
33
|
+
});
|
|
34
|
+
if (event === undefined)
|
|
35
|
+
return unavailable;
|
|
36
|
+
const p = event.payload;
|
|
37
|
+
if (!p.acceptance?.trim()
|
|
38
|
+
|| !["user", "operator", "leader"].includes(p.by ?? "")
|
|
39
|
+
|| !/^[0-9a-f]{64}$/u.test(p.diffDigest ?? "")
|
|
40
|
+
|| !/^[0-9a-f]{40}$/u.test(p.acceptedTree ?? "")
|
|
41
|
+
|| !/^[0-9a-f]{40}$/u.test(p.candidateTree ?? "")) {
|
|
42
|
+
return { event: null, reason: `Adoption ${event.id} has incomplete fixed evidence.` };
|
|
43
|
+
}
|
|
44
|
+
if (p.integrationId === undefined)
|
|
45
|
+
return { event, reason: null };
|
|
46
|
+
const integration = integrations.find(i => i.id === p.integrationId);
|
|
47
|
+
const valid = integration !== undefined && integration.status === "committed"
|
|
48
|
+
&& integration.taskId === publication.taskId
|
|
49
|
+
&& integration.projectId === publication.projectId
|
|
50
|
+
&& integration.afterCommit === publication.localCommit
|
|
51
|
+
&& contextContentDigest(integration) === p.integrationDigest;
|
|
52
|
+
return valid ? { event, reason: null } : {
|
|
53
|
+
event: null,
|
|
54
|
+
reason: `Adoption ${event.id} references Integration ${p.integrationId}, whose committed candidate evidence is missing or changed (${integration?.status ?? "missing"}).`
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -35,6 +35,9 @@ export function createPublicationReference(id, taskId, input, now) {
|
|
|
35
35
|
...(input.localCommit === undefined ? {} : {
|
|
36
36
|
localCommit: requireCommit(input.localCommit, "Publication local commit")
|
|
37
37
|
}),
|
|
38
|
+
...(input.headCommit === undefined ? {} : {
|
|
39
|
+
headCommit: requireCommit(input.headCommit, "Publication remote head")
|
|
40
|
+
}),
|
|
38
41
|
...(input.remoteCommit === undefined ? {} : {
|
|
39
42
|
remoteCommit: requireCommit(input.remoteCommit, "Publication remote commit")
|
|
40
43
|
}),
|
|
@@ -82,6 +85,13 @@ export function validatePublicationReference(reference) {
|
|
|
82
85
|
if (reference.localCommit !== undefined) {
|
|
83
86
|
requireCommit(reference.localCommit, "Publication local commit");
|
|
84
87
|
}
|
|
88
|
+
if (reference.headCommit !== undefined) {
|
|
89
|
+
requireCommit(reference.headCommit, "Publication remote head");
|
|
90
|
+
if (reference.verification === "verified"
|
|
91
|
+
&& reference.headCommit !== reference.localCommit) {
|
|
92
|
+
throw new Error("Verified publication remote head must match its local commit.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
85
95
|
if (reference.remoteCommit !== undefined) {
|
|
86
96
|
requireCommit(reference.remoteCommit, "Publication remote commit");
|
|
87
97
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { publicationAdoption } from "./publicationAdoption.js";
|
|
1
2
|
/**
|
|
2
3
|
* One read-only delivery projection over existing Task, workspace, completion,
|
|
3
4
|
* and Publication facts. It never writes a merged flag or queries a provider.
|
|
@@ -19,13 +20,19 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
19
20
|
.filter((reference) => reference.projectId === binding.projectId);
|
|
20
21
|
const matching = expectedLocalCommit === null
|
|
21
22
|
? []
|
|
22
|
-
: projectPublications.filter((reference) => reference.localCommit === expectedLocalCommit
|
|
23
|
+
: projectPublications.filter((reference) => reference.localCommit === expectedLocalCommit
|
|
24
|
+
|| publicationAdoption(reference, expected.event, expectedLocalCommit, facts.events, facts.publications, facts.integrations ?? []).event !== null);
|
|
23
25
|
const publication = bestPublication(matching.length > 0 ? matching : projectPublications);
|
|
24
|
-
const
|
|
26
|
+
const adoptionEvidence = publication === null ? null
|
|
27
|
+
: publicationAdoption(publication, expected.event, expectedLocalCommit, facts.events, facts.publications, facts.integrations ?? []);
|
|
28
|
+
const adoption = adoptionEvidence?.event ?? null;
|
|
29
|
+
const covers = expectedLocalCommit !== null && publication !== null
|
|
30
|
+
&& (publication.localCommit === expectedLocalCommit || adoption !== null);
|
|
31
|
+
const headMatches = publication?.headCommit === undefined
|
|
32
|
+
|| publication.headCommit === publication.localCommit;
|
|
33
|
+
const coverage = projectCoverage(codeDelivery, expectedLocalCommit, publication, covers, headMatches);
|
|
25
34
|
const merged = codeDelivery === "none"
|
|
26
|
-
|| (
|
|
27
|
-
&& expectedLocalCommit !== null
|
|
28
|
-
&& publication.localCommit === expectedLocalCommit
|
|
35
|
+
|| (covers && headMatches
|
|
29
36
|
&& publication.state === "merged");
|
|
30
37
|
const verified = codeDelivery === "none"
|
|
31
38
|
|| (merged && publication?.verification === "verified");
|
|
@@ -34,6 +41,8 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
34
41
|
directory: binding.directory,
|
|
35
42
|
baseCommit,
|
|
36
43
|
expectedLocalCommit,
|
|
44
|
+
deliveryLocalCommit: covers ? publication.localCommit ?? null : null,
|
|
45
|
+
adoption,
|
|
37
46
|
codeDelivery,
|
|
38
47
|
publication,
|
|
39
48
|
state: publication?.state ?? null,
|
|
@@ -42,7 +51,8 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
42
51
|
coverage,
|
|
43
52
|
merged,
|
|
44
53
|
verified,
|
|
45
|
-
reason: projectCoverageReason(
|
|
54
|
+
reason: projectCoverageReason(expectedLocalCommit, publication, coverage, adoption) + (coverage === "stale" && adoptionEvidence?.reason
|
|
55
|
+
? ` ${adoptionEvidence.reason}` : "")
|
|
46
56
|
};
|
|
47
57
|
});
|
|
48
58
|
const codeProjects = projects.filter((project) => project.codeDelivery !== "none");
|
|
@@ -55,8 +65,9 @@ export function projectTaskRemoteDelivery(facts) {
|
|
|
55
65
|
: codeProjects.some((project) => project.coverage === "head-unavailable")
|
|
56
66
|
? "unavailable"
|
|
57
67
|
: allMerged
|
|
58
|
-
? "merged"
|
|
59
|
-
: mergedProjectCount > 0 ? "partial"
|
|
68
|
+
? allVerified ? "merged" : "unverified"
|
|
69
|
+
: mergedProjectCount > 0 ? "partial"
|
|
70
|
+
: codeProjects.some(p => p.state === "merged") ? "uncovered" : "pending";
|
|
60
71
|
return {
|
|
61
72
|
taskId: facts.task.id,
|
|
62
73
|
source: expected.source,
|
|
@@ -111,7 +122,7 @@ function expectedDelivery(facts) {
|
|
|
111
122
|
commits: new Map()
|
|
112
123
|
};
|
|
113
124
|
}
|
|
114
|
-
function completionEvent(events, completedAt) {
|
|
125
|
+
export function completionEvent(events, completedAt) {
|
|
115
126
|
const exact = latestEvent(events, "task.completed", (event) => (event.createdAt === completedAt));
|
|
116
127
|
return exact ?? latestEvent(events, "task.completed", (event) => (event.createdAt <= completedAt));
|
|
117
128
|
}
|
|
@@ -154,7 +165,7 @@ function candidateCommits(candidate) {
|
|
|
154
165
|
commit.toLowerCase()
|
|
155
166
|
]));
|
|
156
167
|
}
|
|
157
|
-
function commitMap(value) {
|
|
168
|
+
export function commitMap(value) {
|
|
158
169
|
const result = new Map();
|
|
159
170
|
for (const entry of value?.split(",") ?? []) {
|
|
160
171
|
const separator = entry.indexOf("@");
|
|
@@ -188,7 +199,7 @@ function publicationRank(reference) {
|
|
|
188
199
|
return 2;
|
|
189
200
|
return 1;
|
|
190
201
|
}
|
|
191
|
-
function projectCoverage(codeDelivery, expectedLocalCommit, publication) {
|
|
202
|
+
function projectCoverage(codeDelivery, expectedLocalCommit, publication, covers, headMatches) {
|
|
192
203
|
if (codeDelivery === "none")
|
|
193
204
|
return "not-applicable";
|
|
194
205
|
if (expectedLocalCommit === null)
|
|
@@ -197,7 +208,9 @@ function projectCoverage(codeDelivery, expectedLocalCommit, publication) {
|
|
|
197
208
|
return "missing";
|
|
198
209
|
if (publication.localCommit === undefined)
|
|
199
210
|
return "missing-local-commit";
|
|
200
|
-
if (
|
|
211
|
+
if (!headMatches)
|
|
212
|
+
return "head-mismatch";
|
|
213
|
+
if (!covers)
|
|
201
214
|
return "stale";
|
|
202
215
|
if (publication.state === "open")
|
|
203
216
|
return "open";
|
|
@@ -205,7 +218,7 @@ function projectCoverage(codeDelivery, expectedLocalCommit, publication) {
|
|
|
205
218
|
return "closed";
|
|
206
219
|
return publication.verification === "verified" ? "verified" : "merged";
|
|
207
220
|
}
|
|
208
|
-
function projectCoverageReason(
|
|
221
|
+
function projectCoverageReason(expectedLocalCommit, publication, coverage, adoption) {
|
|
209
222
|
switch (coverage) {
|
|
210
223
|
case "not-applicable":
|
|
211
224
|
return "Task head equals the managed workspace base; no code delivery is required.";
|
|
@@ -216,15 +229,17 @@ function projectCoverageReason(codeDelivery, expectedLocalCommit, publication, c
|
|
|
216
229
|
case "missing-local-commit":
|
|
217
230
|
return `Publication ${publication.id} does not record a local commit.`;
|
|
218
231
|
case "stale":
|
|
219
|
-
return `Publication ${publication.id}
|
|
232
|
+
return `Publication ${publication.id} is ${publication.state}, but its local candidate ${publication.localCommit} has no valid adoption covering accepted Task head ${expectedLocalCommit}.`;
|
|
233
|
+
case "head-mismatch":
|
|
234
|
+
return `Publication ${publication.id} is ${publication.state}, but observed remote head ${publication.headCommit} differs from local candidate ${publication.localCommit}; verification is invalid.`;
|
|
220
235
|
case "open":
|
|
221
236
|
return `Publication ${publication.id} covers the Task head but is still open.`;
|
|
222
237
|
case "closed":
|
|
223
238
|
return `Publication ${publication.id} covers the Task head but is closed without merge.`;
|
|
224
239
|
case "merged":
|
|
225
|
-
return `Publication ${publication.id} reports
|
|
240
|
+
return `Publication ${publication.id} reports a covering candidate as merged, but remote verification is still required.`;
|
|
226
241
|
case "verified":
|
|
227
|
-
return `Publication ${publication.id} verifies the exact Task head as merged.`;
|
|
242
|
+
return `Publication ${publication.id} verifies ${adoption === null ? "the exact Task head" : `candidate ${publication.localCommit} adopted by ${adoption.id}`} as merged.`;
|
|
228
243
|
}
|
|
229
244
|
}
|
|
230
245
|
function compareCreated(left, right) {
|