@zq-silk/yui 0.8.9 → 0.9.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/ARCHITECTURE.md +48 -46
- package/README.md +33 -31
- package/dist/cli/commandCatalog.js +7 -7
- package/dist/cli.js +1 -31
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/taskCommands.js +110 -307
- package/dist/commands/taskCompletionGate.js +15 -12
- package/dist/commands/taskContextCommand.js +18 -11
- package/dist/commands/taskNextActionCommand.js +4 -5
- package/dist/commands/taskWorkspaceCommands.js +2 -2
- package/dist/execution/executionGroup.js +0 -3
- package/dist/executor/agentExecutor.js +4 -2
- package/dist/executor/effectiveLaunch.js +33 -3
- package/dist/integration/gitIntegrationService.js +1 -1
- package/dist/lifecycle/exactRunTerminalization.js +12 -8
- package/dist/observability/orchestrationMetrics.js +5 -19
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +2 -0
- package/dist/repository/taskWorkspacePreparer.js +173 -26
- package/dist/review/reviewRound.js +41 -24
- package/dist/storage/migration/productionRegistry.js +138 -0
- package/dist/storage/sqliteStore.js +1 -1
- package/dist/storage/taskStore.js +26 -27
- package/dist/task/completionReadiness.js +24 -22
- package/dist/task/nextAction.js +47 -61
- package/dist/task/task.js +12 -19
- package/dist/web/assets/client/components.js +3 -1
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/webSnapshot.js +0 -3
- package/i18n/README.zh-CN.md +18 -19
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +47 -43
- package/skills/yui-operator/SKILL.md +24 -33
- package/skills/yui-reviewer/SKILL.md +18 -12
- package/skills/yui-worker/SKILL.md +5 -3
|
@@ -62,13 +62,13 @@ export const CURRENT_PROJECT_SCHEMA_VERSION = 4;
|
|
|
62
62
|
export const CURRENT_AGENT_PROFILE_SCHEMA_VERSION = 2;
|
|
63
63
|
export const CURRENT_GLOBAL_ROLE_SCHEMA_VERSION = 3;
|
|
64
64
|
export const CURRENT_GLOBAL_ROLE_SESSION_SET_SCHEMA_VERSION = 3;
|
|
65
|
-
export const CURRENT_TASK_SCHEMA_VERSION =
|
|
65
|
+
export const CURRENT_TASK_SCHEMA_VERSION = 5;
|
|
66
66
|
export const CURRENT_TASK_BRIEF_SCHEMA_VERSION = 2;
|
|
67
67
|
export const CURRENT_CONTEXT_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
68
68
|
export const CURRENT_TASK_ROLE_SCHEMA_VERSION = 3;
|
|
69
69
|
export const CURRENT_MANAGED_WORKSPACE_SCHEMA_VERSION = 2;
|
|
70
70
|
export const CURRENT_WORK_ITEM_SCHEMA_VERSION = 9;
|
|
71
|
-
export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION =
|
|
71
|
+
export const CURRENT_REVIEW_ROUND_SCHEMA_VERSION = 5;
|
|
72
72
|
export const CURRENT_CHANGE_SET_SCHEMA_VERSION = 3;
|
|
73
73
|
export const CURRENT_INTEGRATION_ATTEMPT_SCHEMA_VERSION = 4;
|
|
74
74
|
export const CURRENT_MESSAGE_SCHEMA_VERSION = 3;
|
|
@@ -417,7 +417,7 @@ export class FileTaskStore {
|
|
|
417
417
|
id: aggregate.task.id,
|
|
418
418
|
status: aggregate.task.status,
|
|
419
419
|
projectBindings: aggregate.task.projectBindings,
|
|
420
|
-
|
|
420
|
+
type: aggregate.task.type
|
|
421
421
|
},
|
|
422
422
|
workItems: values(aggregate.workItems, "id"),
|
|
423
423
|
changeSets: values(aggregate.changeSets, "id"),
|
|
@@ -1026,18 +1026,16 @@ export class FileTaskStore {
|
|
|
1026
1026
|
throw new StorageRecordError(`ReviewRound belongs to another Task: ${stored.taskId}.`);
|
|
1027
1027
|
}
|
|
1028
1028
|
const aggregate = this.#requireTaskForWrite(taskId);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
&& !sameTaskFinalReviewContract(stored.taskFinalReviewContract, candidate.taskFinalReviewContract)) {
|
|
1040
|
-
throw new StorageRecordError(`Task ReviewRound contract does not match its Candidate: ${stored.id}.`);
|
|
1029
|
+
if ((stored.scope ?? "work-item") === "work-item") {
|
|
1030
|
+
const item = aggregate.workItems[stored.workItemId];
|
|
1031
|
+
if (item === undefined) {
|
|
1032
|
+
throw new StorageRecordError(`ReviewRound Work Item not found: ${stored.workItemId}.`);
|
|
1033
|
+
}
|
|
1034
|
+
const candidate = item.candidates.find(({ id }) => id === stored.candidateId);
|
|
1035
|
+
if (candidate === undefined) {
|
|
1036
|
+
throw new StorageRecordError(`ReviewRound Candidate not found: ${stored.candidateId}.`);
|
|
1037
|
+
}
|
|
1038
|
+
assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${stored.id}`);
|
|
1041
1039
|
}
|
|
1042
1040
|
if (stored.reviewerRunId !== undefined) {
|
|
1043
1041
|
const reviewerRun = aggregate.agentRuns[stored.reviewerRunId];
|
|
@@ -3349,24 +3347,23 @@ function validateCanonicalTaskReferences(state, aggregate) {
|
|
|
3349
3347
|
}
|
|
3350
3348
|
}
|
|
3351
3349
|
for (const round of Object.values(aggregate.reviewRounds)) {
|
|
3352
|
-
const item = aggregate.workItems[round.workItemId];
|
|
3353
|
-
if (item === undefined) {
|
|
3354
|
-
throw new StorageRecordError(`ReviewRound references are invalid: ${round.id}.`);
|
|
3355
|
-
}
|
|
3356
|
-
const candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
3357
|
-
if (candidate === undefined) {
|
|
3358
|
-
throw new StorageRecordError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
3359
|
-
}
|
|
3360
|
-
assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${round.id}`);
|
|
3361
3350
|
if ((round.scope ?? "work-item") === "task") {
|
|
3362
3351
|
const frozenProjects = round.taskCandidate?.projects ?? [];
|
|
3363
3352
|
if (frozenProjects.length !== boundProjects.size
|
|
3364
3353
|
|| frozenProjects.some(({ projectId }) => !boundProjects.has(projectId))) {
|
|
3365
3354
|
throw new StorageRecordError(`Task ReviewRound Projects do not match Task scope: ${round.id}.`);
|
|
3366
3355
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3356
|
+
}
|
|
3357
|
+
else {
|
|
3358
|
+
const item = aggregate.workItems[round.workItemId];
|
|
3359
|
+
if (item === undefined) {
|
|
3360
|
+
throw new StorageRecordError(`ReviewRound references are invalid: ${round.id}.`);
|
|
3361
|
+
}
|
|
3362
|
+
const candidate = item.candidates.find(({ id }) => id === round.candidateId);
|
|
3363
|
+
if (candidate === undefined) {
|
|
3364
|
+
throw new StorageRecordError(`ReviewRound Candidate not found: ${round.candidateId}.`);
|
|
3369
3365
|
}
|
|
3366
|
+
assertWorkItemCandidateReferences(aggregate, item, candidate, `ReviewRound candidate ${round.id}`);
|
|
3370
3367
|
}
|
|
3371
3368
|
if (round.reviewerRunId !== undefined) {
|
|
3372
3369
|
const reviewerRun = aggregate.agentRuns[round.reviewerRunId];
|
|
@@ -3496,7 +3493,9 @@ function assertManagedWorkspaceReferences(aggregate, workspace, label, workItemO
|
|
|
3496
3493
|
if (workspace.entries.some(({ access }) => access !== "write")) {
|
|
3497
3494
|
throw new StorageRecordError(`${label} ReviewRound workspace must be writable: ${taskId}/${round.id}.`);
|
|
3498
3495
|
}
|
|
3499
|
-
const item =
|
|
3496
|
+
const item = round.workItemId === undefined
|
|
3497
|
+
? undefined
|
|
3498
|
+
: aggregate.workItems[round.workItemId];
|
|
3500
3499
|
const candidate = item?.candidates.find(({ id }) => id === round.candidateId);
|
|
3501
3500
|
const frozenProjects = round.scope === "task"
|
|
3502
3501
|
? round.taskCandidate?.projects
|
|
@@ -97,33 +97,35 @@ export function projectCompletionReadiness(facts, options = {}) {
|
|
|
97
97
|
fix: `wait for DurableJob ${job.id} to finish, or cancel it`
|
|
98
98
|
});
|
|
99
99
|
}
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
blockers.push({
|
|
112
|
-
code: "integration-evidence-missing",
|
|
113
|
-
ref: ref("task", task.id),
|
|
114
|
-
reason: `Task ${task.id} requires at least one ChangeSet before completion.`,
|
|
115
|
-
fix: `yui task work capture ${task.id}/<work-item>`
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
if (!facts.integrations.some(({ status }) => status === "committed")) {
|
|
100
|
+
// Integration obligations are derived from real independent delivery units.
|
|
101
|
+
// A Leader-owned Task needs no synthetic WorkItem; once a WorkItem produces a
|
|
102
|
+
// Git Candidate, that exact unit must be captured and integrated.
|
|
103
|
+
for (const item of facts.workItems) {
|
|
104
|
+
if (item.status !== "completed")
|
|
105
|
+
continue;
|
|
106
|
+
const candidate = item.candidates?.at(-1);
|
|
107
|
+
const hasGitDelivery = candidate?.workspace !== undefined
|
|
108
|
+
|| candidate?.gitSnapshot !== undefined;
|
|
109
|
+
if (hasGitDelivery
|
|
110
|
+
&& !facts.changeSets.some((changeSet) => changeSet.workItemId === item.id)) {
|
|
119
111
|
blockers.push({
|
|
120
112
|
code: "integration-evidence-missing",
|
|
121
|
-
ref: ref("
|
|
122
|
-
reason: `
|
|
123
|
-
fix: `yui task
|
|
113
|
+
ref: ref("work-item", item.id),
|
|
114
|
+
reason: `Completed WorkItem ${item.id} has an uncaptured Git delivery.`,
|
|
115
|
+
fix: `yui task work capture ${task.id}/${item.id}`
|
|
124
116
|
});
|
|
125
117
|
}
|
|
126
118
|
}
|
|
119
|
+
for (const changeSet of facts.changeSets) {
|
|
120
|
+
if (facts.integrations.some((attempt) => (attempt.status === "committed" && attempt.changeSetIds.includes(changeSet.id))))
|
|
121
|
+
continue;
|
|
122
|
+
blockers.push({
|
|
123
|
+
code: "integration-evidence-missing",
|
|
124
|
+
ref: ref("change-set", changeSet.id),
|
|
125
|
+
reason: `ChangeSet ${changeSet.id} is not part of a committed Integration.`,
|
|
126
|
+
fix: `yui task integration start ${task.id} --project ${changeSet.projectId} --change-set ${changeSet.id}`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
127
129
|
// Unresolved Integration Attempts must settle.
|
|
128
130
|
for (const integration of facts.integrations) {
|
|
129
131
|
if (!UNRESOLVED_INTEGRATION_STATUSES.has(integration.status))
|
package/dist/task/nextAction.js
CHANGED
|
@@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { deltaRecheckBlocksAcceptance } from "../review/reviewRound.js";
|
|
3
3
|
import { classifyReviewRoundOutcome, isSemanticReviewRound } from "../review/reviewOutcomeClassifier.js";
|
|
4
4
|
import { resolveRecordedTaskFinalReviewContract } from "../review/taskFinalReviewContractRebind.js";
|
|
5
|
-
import { taskDeliveryPath } from "./task.js";
|
|
6
5
|
import { currentWorkItemCandidate, governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
7
6
|
const OPEN_WORK_ITEM_STATUSES = new Set(["pending", "running", "awaiting_acceptance"]);
|
|
8
7
|
export function projectNextAction(facts) {
|
|
@@ -70,7 +69,7 @@ export function projectNextAction(facts) {
|
|
|
70
69
|
const candidateRef = ref("candidate", `${candidateReady.id}/${candidate.id}`);
|
|
71
70
|
return buildAction(facts, {
|
|
72
71
|
kind: "repair-protocol-inconsistency",
|
|
73
|
-
reason: `Candidate ${candidate.id} is a
|
|
72
|
+
reason: `Candidate ${candidate.id} is a Task-main delivery with base==head (no commits); reject it and re-dispatch real work.`,
|
|
74
73
|
refs: [ref("work-item", candidateReady.id), candidateRef],
|
|
75
74
|
conflicts: [candidateRef],
|
|
76
75
|
preconditions: [
|
|
@@ -254,42 +253,35 @@ export function projectNextAction(facts) {
|
|
|
254
253
|
judgmentRequired: "Leader must choose the execution path: direct execution, a native subagent, or managed Task Role dispatch."
|
|
255
254
|
});
|
|
256
255
|
}
|
|
257
|
-
if (facts.workItems.length === 0) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
preconditions: [
|
|
287
|
-
{ fact: "At least one Work Item exists", satisfied: false },
|
|
288
|
-
{ fact: "Task is active", satisfied: task.status === "active" }
|
|
289
|
-
],
|
|
290
|
-
recommendedCommand: `yui task work create ${task.id} \"<objective>\"`
|
|
291
|
-
});
|
|
292
|
-
}
|
|
256
|
+
if (facts.workItems.length === 0 && !taskFinalReviewRequired(facts)) {
|
|
257
|
+
const reviewAlternative = facts.reviewConfig === null
|
|
258
|
+
? []
|
|
259
|
+
: [{
|
|
260
|
+
kind: "request-final-review",
|
|
261
|
+
reason: "Request one independent Review of the frozen Task result when risk warrants it.",
|
|
262
|
+
recommendedCommand: `yui task review request ${task.id} --role ${facts.reviewConfig.roleName}`,
|
|
263
|
+
refs: [ref("task", task.id)]
|
|
264
|
+
}];
|
|
265
|
+
return buildAction(facts, {
|
|
266
|
+
kind: "complete-task",
|
|
267
|
+
reason: task.type === "bugfix"
|
|
268
|
+
? `Task ${task.id} is a Leader-owned bugfix; implement and verify it on Task main without manufacturing a WorkItem.`
|
|
269
|
+
: task.type === "feature"
|
|
270
|
+
? `Feature ${task.id} has no independent delivery units; the Leader may implement it on Task main or create WorkItems only if separate ownership is genuinely useful.`
|
|
271
|
+
: `Task ${task.id} has no independent delivery units; the Leader decides whether to own it on Task main or create WorkItems only if separate ownership is genuinely useful.`,
|
|
272
|
+
refs: [ref("task", task.id)],
|
|
273
|
+
preconditions: [
|
|
274
|
+
{ fact: "Task is active", satisfied: task.status === "active", ref: ref("task", task.id) },
|
|
275
|
+
{ fact: "Task main is clean, committed, and verified", satisfied: false }
|
|
276
|
+
],
|
|
277
|
+
recommendedCommand: `yui task complete ${task.id} --summary-file -`,
|
|
278
|
+
...(reviewAlternative.length === 0 ? {} : { alternatives: reviewAlternative }),
|
|
279
|
+
judgmentRequired: task.type === "bugfix"
|
|
280
|
+
? "Leader must judge whether the bugfix risk warrants one optional final Review."
|
|
281
|
+
: task.type === "feature"
|
|
282
|
+
? "Leader must judge whether this feature is small enough to own directly or needs independently owned WorkItems, and whether the final result warrants Review."
|
|
283
|
+
: "Leader must choose the smallest useful topology from the Project-defined Task intent, then decide whether the frozen result warrants Review."
|
|
284
|
+
});
|
|
293
285
|
}
|
|
294
286
|
const uncaptured = facts.workItems.find((item) => needsChangeSetCapture(facts, item));
|
|
295
287
|
if (uncaptured !== undefined) {
|
|
@@ -428,28 +420,25 @@ export function projectNextAction(facts) {
|
|
|
428
420
|
&& finalReviewRequired
|
|
429
421
|
&& !hasValidFinalReview(facts)) {
|
|
430
422
|
const reviewerRole = taskFinalReviewRole(facts);
|
|
431
|
-
const directWithoutWorkItems = taskDeliveryPath(task) === "direct"
|
|
432
|
-
&& facts.workItems.length === 0;
|
|
433
423
|
return buildAction(facts, {
|
|
434
424
|
kind: "request-final-review",
|
|
435
|
-
reason:
|
|
436
|
-
? "This
|
|
437
|
-
: "All
|
|
425
|
+
reason: facts.workItems.length === 0
|
|
426
|
+
? "This Task already owns a final-Review obligation; completion must prepare or resume a Review of its frozen Task head."
|
|
427
|
+
: "All WorkItems are integrated but no valid Task-final Review attests the frozen Task result.",
|
|
438
428
|
refs: [ref("task", task.id)],
|
|
439
|
-
preconditions:
|
|
429
|
+
preconditions: facts.workItems.length === 0
|
|
440
430
|
? [{ fact: "Valid established Task-final Review at the direct head", satisfied: false }]
|
|
441
431
|
: [
|
|
442
432
|
{ fact: "All Work Items are terminal", satisfied: true },
|
|
443
433
|
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
444
434
|
{ fact: "Valid Task-final Review at the integrated head", satisfied: false }
|
|
445
435
|
],
|
|
446
|
-
recommendedCommand:
|
|
436
|
+
recommendedCommand: facts.workItems.length === 0
|
|
447
437
|
? `yui task complete ${task.id} --summary-file -`
|
|
448
438
|
: `yui task review request ${task.id} --role ${reviewerRole ?? "<reviewer-role>"}`
|
|
449
439
|
});
|
|
450
440
|
}
|
|
451
|
-
const finalReviewOptional =
|
|
452
|
-
&& !finalReviewRequired
|
|
441
|
+
const finalReviewOptional = !finalReviewRequired
|
|
453
442
|
&& !hasValidFinalReview(facts);
|
|
454
443
|
const finalReviewAlternative = finalReviewOptional && facts.reviewConfig !== null
|
|
455
444
|
? [{
|
|
@@ -461,19 +450,16 @@ export function projectNextAction(facts) {
|
|
|
461
450
|
: [];
|
|
462
451
|
return buildAction(facts, {
|
|
463
452
|
kind: "complete-task",
|
|
464
|
-
reason:
|
|
465
|
-
? "The
|
|
466
|
-
: "
|
|
453
|
+
reason: facts.workItems.length === 0
|
|
454
|
+
? "The Leader-owned Task result and its established obligations are ready; complete it without creating successor work."
|
|
455
|
+
: "Every independent delivery unit is integrated; complete the Task instead of creating successor work.",
|
|
467
456
|
refs: [ref("task", task.id)],
|
|
468
457
|
preconditions: [
|
|
469
458
|
{ fact: "All Work Items are terminal", satisfied: true },
|
|
470
459
|
...(task.projectBindings.length === 0
|
|
471
460
|
? []
|
|
472
|
-
:
|
|
473
|
-
? [{
|
|
474
|
-
fact: "Valid established Task-final Review at the direct head",
|
|
475
|
-
satisfied: hasValidFinalReview(facts)
|
|
476
|
-
}]
|
|
461
|
+
: facts.workItems.length === 0
|
|
462
|
+
? [{ fact: "Task main is clean, committed, and verified", satisfied: false }]
|
|
477
463
|
: [
|
|
478
464
|
{ fact: "Every ChangeSet is committed", satisfied: true },
|
|
479
465
|
{
|
|
@@ -486,7 +472,7 @@ export function projectNextAction(facts) {
|
|
|
486
472
|
...(!finalReviewOptional
|
|
487
473
|
? {}
|
|
488
474
|
: {
|
|
489
|
-
judgmentRequired: "Leader must decide whether the
|
|
475
|
+
judgmentRequired: "Leader must decide whether the frozen Task result is safe to complete or needs one optional Task-final Review."
|
|
490
476
|
}),
|
|
491
477
|
recommendedCommand: `yui task complete ${task.id} --summary-file -`
|
|
492
478
|
});
|
|
@@ -685,9 +671,7 @@ function taskFinalReviewContractResolution(facts) {
|
|
|
685
671
|
}
|
|
686
672
|
function taskFinalReviewRequired(facts) {
|
|
687
673
|
return taskFinalReviewContract(facts) !== undefined
|
|
688
|
-
|| latestTaskFinalReview(facts.reviewRounds) !== undefined
|
|
689
|
-
|| (taskDeliveryPath(facts.task) === "integrated"
|
|
690
|
-
&& facts.reviewConfig?.trigger === "final");
|
|
674
|
+
|| latestTaskFinalReview(facts.reviewRounds) !== undefined;
|
|
691
675
|
}
|
|
692
676
|
function taskFinalReviewRole(facts) {
|
|
693
677
|
return taskFinalReviewContract(facts)?.reviewerRoleName
|
|
@@ -724,7 +708,7 @@ function needsChangeSetCapture(facts, item) {
|
|
|
724
708
|
const candidate = item.candidates.at(-1);
|
|
725
709
|
if (candidate === undefined)
|
|
726
710
|
return false;
|
|
727
|
-
// A metadata-only
|
|
711
|
+
// A metadata-only Task-main Candidate has no WorkItem Develop
|
|
728
712
|
// workspace to capture; its boundary is the Task-main head itself.
|
|
729
713
|
if (candidate.workspace === undefined
|
|
730
714
|
&& candidate.gitSnapshot === undefined
|
|
@@ -750,7 +734,7 @@ function hasValidFinalReview(facts) {
|
|
|
750
734
|
.filter((changeSet) => attempt.changeSetIds.includes(changeSet.id))
|
|
751
735
|
.map((changeSet) => changeSet.headCommit)));
|
|
752
736
|
if (integratedHeads.size === 0)
|
|
753
|
-
return
|
|
737
|
+
return facts.workItems.length === 0;
|
|
754
738
|
for (const head of integratedHeads) {
|
|
755
739
|
if (!reviewedCommits.has(head))
|
|
756
740
|
return false;
|
|
@@ -816,6 +800,8 @@ function detectProtocolInconsistency(facts) {
|
|
|
816
800
|
for (const round of facts.reviewRounds) {
|
|
817
801
|
if (round.status !== "pending" && round.status !== "running")
|
|
818
802
|
continue;
|
|
803
|
+
if (round.workItemId === undefined)
|
|
804
|
+
continue;
|
|
819
805
|
const item = workItemById.get(round.workItemId);
|
|
820
806
|
if (item !== undefined && item.status === "retired") {
|
|
821
807
|
return {
|
package/dist/task/task.js
CHANGED
|
@@ -2,7 +2,7 @@ import { validateTaskWorkspaceIdentity } from "../repository/taskWorkspaceIdenti
|
|
|
2
2
|
export function createTask(id, title, now, metadata = {}) {
|
|
3
3
|
const timestamp = now.toISOString();
|
|
4
4
|
return {
|
|
5
|
-
schemaVersion:
|
|
5
|
+
schemaVersion: 5,
|
|
6
6
|
id: requireSafeIdentity(id, "Task id"),
|
|
7
7
|
title: requireText(title, "Task title"),
|
|
8
8
|
...cloneMetadata(metadata),
|
|
@@ -141,6 +141,7 @@ export function updateTaskMetadata(task, metadata, now) {
|
|
|
141
141
|
const updated = { ...task, updatedAt: now.toISOString() };
|
|
142
142
|
if (metadata.title !== undefined)
|
|
143
143
|
updated.title = requireText(metadata.title, "Task title");
|
|
144
|
+
applyOptional(updated, "type", metadata.type);
|
|
144
145
|
applyOptional(updated, "description", metadata.description);
|
|
145
146
|
applyOptional(updated, "priority", metadata.priority);
|
|
146
147
|
applyOptional(updated, "tags", metadata.tags === undefined || metadata.tags === null
|
|
@@ -152,8 +153,6 @@ export function updateTaskMetadata(task, metadata, now) {
|
|
|
152
153
|
}
|
|
153
154
|
if (metadata.cwd !== undefined)
|
|
154
155
|
updated.cwd = requireText(metadata.cwd, "Task workspace");
|
|
155
|
-
if (metadata.requireIntegration === true)
|
|
156
|
-
updated.requireIntegration = true;
|
|
157
156
|
return updated;
|
|
158
157
|
}
|
|
159
158
|
function applyOptional(task, key, value) {
|
|
@@ -171,10 +170,12 @@ export function isTaskArchived(task) {
|
|
|
171
170
|
return task.status === "archived";
|
|
172
171
|
}
|
|
173
172
|
export function validateTask(task) {
|
|
174
|
-
if (task.schemaVersion !==
|
|
175
|
-
throw new Error("Task must use schemaVersion
|
|
173
|
+
if (task.schemaVersion !== 5)
|
|
174
|
+
throw new Error("Task must use schemaVersion 5.");
|
|
176
175
|
requireSafeIdentity(task.id, "Task id");
|
|
177
176
|
requireText(task.title, "Task title");
|
|
177
|
+
if (task.type !== undefined)
|
|
178
|
+
requireSafeIdentity(task.type, "Task type");
|
|
178
179
|
if (!["draft", "active", "completed", "retired", "archived"].includes(task.status)) {
|
|
179
180
|
throw new Error(`Task status is invalid: ${String(task.status)}.`);
|
|
180
181
|
}
|
|
@@ -206,8 +207,10 @@ export function validateTask(task) {
|
|
|
206
207
|
normalizeProjectBindings(task.projectBindings);
|
|
207
208
|
if (task.cwd !== undefined)
|
|
208
209
|
requireText(task.cwd, "Task workspace");
|
|
209
|
-
if (task.
|
|
210
|
-
|
|
210
|
+
if (task.legacyDeliveryPath !== undefined
|
|
211
|
+
&& task.legacyDeliveryPath !== "direct"
|
|
212
|
+
&& task.legacyDeliveryPath !== "integrated") {
|
|
213
|
+
throw new Error("Task legacy delivery path is invalid.");
|
|
211
214
|
}
|
|
212
215
|
const completionFields = [task.completedAt, task.completedBy, task.completionSummary];
|
|
213
216
|
const hasAnyCompletion = completionFields.some((value) => value !== undefined);
|
|
@@ -283,13 +286,13 @@ export function validateTask(task) {
|
|
|
283
286
|
}
|
|
284
287
|
function cloneMetadata(metadata) {
|
|
285
288
|
const cloned = {
|
|
289
|
+
...(metadata.type === undefined ? {} : { type: requireSafeIdentity(metadata.type, "Task type") }),
|
|
286
290
|
...(metadata.description === undefined ? {} : { description: metadata.description }),
|
|
287
291
|
...(metadata.priority === undefined ? {} : { priority: metadata.priority }),
|
|
288
292
|
...(metadata.tags === undefined ? {} : { tags: [...metadata.tags] }),
|
|
289
293
|
...(metadata.dueAt === undefined ? {} : { dueAt: metadata.dueAt }),
|
|
290
294
|
projectBindings: normalizeProjectBindings(metadata.projectBindings ?? []),
|
|
291
|
-
...(metadata.cwd === undefined ? {} : { cwd: requireText(metadata.cwd, "Task workspace") })
|
|
292
|
-
...(metadata.requireIntegration === true ? { requireIntegration: true } : {})
|
|
295
|
+
...(metadata.cwd === undefined ? {} : { cwd: requireText(metadata.cwd, "Task workspace") })
|
|
293
296
|
};
|
|
294
297
|
return cloned;
|
|
295
298
|
}
|
|
@@ -319,16 +322,6 @@ export function taskProjectBinding(task, projectId) {
|
|
|
319
322
|
export function taskHasProjects(task) {
|
|
320
323
|
return task.projectBindings.length > 0;
|
|
321
324
|
}
|
|
322
|
-
/**
|
|
323
|
-
* Delivery is a projection of the existing Task contract, not a second source
|
|
324
|
-
* of truth. Keeping it derived preserves every stored Task schema while giving
|
|
325
|
-
* callers one product-level name for the three supported paths.
|
|
326
|
-
*/
|
|
327
|
-
export function taskDeliveryPath(task) {
|
|
328
|
-
if (task.projectBindings.length === 0)
|
|
329
|
-
return "no-project";
|
|
330
|
-
return task.requireIntegration === true ? "integrated" : "direct";
|
|
331
|
-
}
|
|
332
325
|
export function taskProjectIds(task) {
|
|
333
326
|
return task.projectBindings.map(({ projectId }) => projectId);
|
|
334
327
|
}
|
|
@@ -577,7 +577,9 @@ export function reviewCard(round, t, locale) {
|
|
|
577
577
|
card.append(head);
|
|
578
578
|
const meta = node("div", "record-meta");
|
|
579
579
|
meta.append(node("span", "meta-name", round.reviewerRoleName));
|
|
580
|
-
|
|
580
|
+
if (round.workItemId && round.candidateId) {
|
|
581
|
+
meta.append(node("span", "mono", round.workItemId + " · " + round.candidateId));
|
|
582
|
+
}
|
|
581
583
|
if (round.createdAt) meta.append(node("time", "", formatDateTime(round.createdAt, locale)));
|
|
582
584
|
meta.append(node("span", "", t("detail.reviewBase") + " · " + round.reviewBaseCommit));
|
|
583
585
|
if (round.workspace && round.workspace.root) {
|
|
@@ -106,6 +106,9 @@ const messages = {
|
|
|
106
106
|
"detail.writeProjects": "Writable Projects",
|
|
107
107
|
"disposition.abandoned": "Abandoned",
|
|
108
108
|
"disposition.integrated": "Integrated",
|
|
109
|
+
"disposition.preserved": "Preserved",
|
|
110
|
+
"disposition.reassigned": "Continued by a newer review",
|
|
111
|
+
"disposition.removed": "Removed",
|
|
109
112
|
"empty.brief": "No task brief has been recorded.",
|
|
110
113
|
"empty.none": "No items",
|
|
111
114
|
"empty.runs": "No runs yet.",
|
|
@@ -406,6 +409,9 @@ const messages = {
|
|
|
406
409
|
"detail.writeProjects": "可写项目",
|
|
407
410
|
"disposition.abandoned": "已废弃",
|
|
408
411
|
"disposition.integrated": "已集成",
|
|
412
|
+
"disposition.preserved": "已保留",
|
|
413
|
+
"disposition.reassigned": "已由后续评审继续使用",
|
|
414
|
+
"disposition.removed": "已移除",
|
|
409
415
|
"empty.brief": "尚未记录任务简报。",
|
|
410
416
|
"empty.none": "暂无条目",
|
|
411
417
|
"empty.runs": "暂无运行记录。",
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { taskDeliveryPath } from "../task/task.js";
|
|
2
1
|
import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
2
|
import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
|
|
4
3
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -41,7 +40,6 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
|
|
|
41
40
|
});
|
|
42
41
|
return {
|
|
43
42
|
...task,
|
|
44
|
-
deliveryPath: taskDeliveryPath(task),
|
|
45
43
|
...(names.length === 0 ? {} : { projectNames: names }),
|
|
46
44
|
workItems: countWorkItems(reader.listWorkItems(task.id)),
|
|
47
45
|
roleCount: reader.listRoles(task.id).length,
|
|
@@ -110,7 +108,6 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
|
|
|
110
108
|
return {
|
|
111
109
|
task: {
|
|
112
110
|
...task,
|
|
113
|
-
deliveryPath: taskDeliveryPath(task),
|
|
114
111
|
...(projectNames.length === 0 ? {} : { projectNames })
|
|
115
112
|
},
|
|
116
113
|
execution: buildTaskExecutionProjection(reader, taskId),
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -106,8 +106,8 @@ yui project add app /absolute/workspace/app \
|
|
|
106
106
|
yui project update app --alias app-cli --development develop
|
|
107
107
|
yui project list
|
|
108
108
|
|
|
109
|
-
yui task create "修复 CSV 转义" --project app --
|
|
110
|
-
yui task create "交付 CSV 导出" --project app --
|
|
109
|
+
yui task create "修复 CSV 转义" --project app --type bugfix
|
|
110
|
+
yui task create "交付 CSV 导出" --project app --type feature
|
|
111
111
|
yui task update <task-id> --priority high --tags release,csv --due-at 2026-08-01T00:00:00Z
|
|
112
112
|
yui task update <task-id> --clear-priority --clear-tags --clear-due-at
|
|
113
113
|
yui task show <task-id>
|
|
@@ -115,15 +115,13 @@ yui task context <task-id>
|
|
|
115
115
|
yui task activate <task-id>
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
Project
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
一旦已有提交或交付证据,提升会 fail closed,避免早期修改失去 ChangeSet provenance。
|
|
126
|
-
旧的 `--require-integration` 等价于 `--delivery integrated`,且 integrated 不可降级。
|
|
118
|
+
Task type 描述需求意图,不选择执行协议。软件 Project 通常使用 `bugfix` 或
|
|
119
|
+
`feature`:bugfix 由 Leader 在 Task main 独立、快速完成;如果范围扩大到需要
|
|
120
|
+
独立 owner,应先改为 feature 再创建 WorkItem。feature 由 Leader 判断是自己直接
|
|
121
|
+
交付,还是拆成由不同 Worker 独立负责、可并行推进的较大
|
|
122
|
+
WorkItem。实现步骤、测试、review finding 和局部修复都不是 WorkItem。只有当一项
|
|
123
|
+
需求本身具有独立 owner 和可验收结果时才创建 WorkItem;一旦 WorkItem 产生 Git
|
|
124
|
+
结果,就必须通过 ChangeSet 和 committed Integration 汇总回 Leader 的 Task main。
|
|
127
125
|
|
|
128
126
|
面向用户的时间默认按北京时间(`Asia/Shanghai`)显示;持久化记录和
|
|
129
127
|
`--json` 数据仍使用 UTC/RFC 3339。可通过以下命令查看或修改 IANA 时区:
|
|
@@ -142,9 +140,8 @@ yui config show
|
|
|
142
140
|
yui config workflow clear review
|
|
143
141
|
```
|
|
144
142
|
|
|
145
|
-
对带 Project 的软件交付,可使用 `--trigger final
|
|
146
|
-
|
|
147
|
-
一次 Task 级 ReviewRound:
|
|
143
|
+
对带 Project 的软件交付,可使用 `--trigger final` 提供默认 Reviewer Role;
|
|
144
|
+
是否需要独立的 Task-final Review 仍由 Leader 根据风险判断:
|
|
148
145
|
|
|
149
146
|
```sh
|
|
150
147
|
yui config workflow set review --role reviewer --trigger final
|
|
@@ -161,11 +158,13 @@ AgentRun 不创建新 WorkItem,也不会递归触发审查。审查以自然
|
|
|
161
158
|
唤醒 Leader;Leader 决定验收、reject 后在原 Role 与原 Session 中修复、
|
|
162
159
|
再次审查,或通过 InputRequest 询问用户。审查失败会保留为可见证据并
|
|
163
160
|
唤醒 Leader,但不会取代 Leader 的最终判断。
|
|
164
|
-
`final` 不为每个 WorkItem 创建完整 ReviewRound
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
`final` 不为每个 WorkItem 创建完整 ReviewRound,也不决定 Task 拓扑。Leader
|
|
162
|
+
显式请求 Task 级 Review;不可变 Task contract 也可以强制要求。Task-final Round
|
|
163
|
+
直接冻结 Task main,不需要虚构 WorkItem/Candidate,因此没有 WorkItem 的小任务也能
|
|
164
|
+
review。冻结头变化时创建新的语义 Round;同一 Reviewer 的兼容原生 Session 可以在
|
|
165
|
+
稳定 workspace 中继续,而每个 Run 仍严格绑定自己的 Round 和冻结头。旧报告保留为
|
|
166
|
+
证据。Reviewer 按 Project Policy/Knowledge 检查整个 Task,并只报告有直接证据的
|
|
167
|
+
可达、重要、可行动问题或有限验证缺口。
|
|
169
168
|
所有候选、ReviewRound 和 Leader 决策都集中在原 WorkItem 下;reject
|
|
170
169
|
后的下一轮会复用原执行 Role、Session 与 workspace,并追加新候选。
|
|
171
170
|
|