@zq-silk/yui 0.6.13 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +173 -57
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +99 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +504 -2
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +68 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +11 -1
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +232 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +199 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +1 -0
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +282 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +13 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -44,6 +44,7 @@ export function runTaskContextCommand(args, store) {
44
44
  const reviewRounds = chronological(reader.listReviewRounds(task.id));
45
45
  const changeSets = chronological(reader.listChangeSets(task.id));
46
46
  const integrations = chronological(reader.listIntegrationAttempts(task.id));
47
+ const publications = chronological(reader.listPublicationReferences(task.id));
47
48
  const nextActionFacts = reader.readNextActionFacts(task.id);
48
49
  if (nextActionFacts === null) {
49
50
  throw new Error(`Task next-action facts disappeared: ${task.id}.`);
@@ -66,6 +67,7 @@ export function runTaskContextCommand(args, store) {
66
67
  reviewRounds,
67
68
  changeSets,
68
69
  integrations,
70
+ publications,
69
71
  messages: reader.listMessages(task.id),
70
72
  openInputRequests: inputRequests.filter((request) => request.status === "open"),
71
73
  resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
@@ -73,7 +75,7 @@ export function runTaskContextCommand(args, store) {
73
75
  nextAction: projectNextAction(nextActionFacts)
74
76
  };
75
77
  });
76
- const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
78
+ const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
77
79
  const timeZone = store.getConfig().timeZone;
78
80
  const displayedActiveDecisions = activeDecisions.slice(-RECENT_RECORD_LIMIT);
79
81
  const displayedWorkItems = currentAndRecentWorkItems(workItems);
@@ -283,6 +285,24 @@ export function runTaskContextCommand(args, store) {
283
285
  ? [" None."]
284
286
  : integrations.slice(-RECENT_RECORD_LIMIT).map((integration) => (` ${integration.id} [${integration.status}/${integration.projectId}] — ${integration.targetRef}; ${integration.changeSetIds.join(", ")}`))),
285
287
  "",
288
+ `Publication references (${publications.length}):`,
289
+ ...(publications.length === 0
290
+ ? [" None."]
291
+ : publications.slice(-RECENT_RECORD_LIMIT).map((reference) => [
292
+ ` ${reference.id}: ${reference.provider}/${reference.repository}/${reference.externalId}`,
293
+ ...(reference.title === undefined ? [] : [` Title: ${reference.title}`]),
294
+ ...(reference.sourceBranch === undefined && reference.targetBranch === undefined
295
+ ? []
296
+ : [` Branches: ${reference.sourceBranch ?? "unknown"} -> ${reference.targetBranch ?? "unknown"}`]),
297
+ ` ${reference.state}/${reference.verification}; ${reference.localCommit ?? "unknown"} -> ${reference.remoteCommit ?? "unknown"}`,
298
+ ...(reference.mergedAt === undefined
299
+ ? []
300
+ : [` Merged: ${formatTimestamp(reference.mergedAt, timeZone)}`]),
301
+ ...(reference.supersedes === undefined
302
+ ? []
303
+ : [` Supersedes: ${reference.supersedes}`])
304
+ ]).flat()),
305
+ "",
286
306
  ...recentSection("messages", messages, (message) => [
287
307
  ` ${message.id} [${taskMessageAuthorLabel(message.author)}] ${formatTimestamp(message.createdAt, timeZone)}`,
288
308
  ` ${compactText(message.body)}`
@@ -342,7 +362,21 @@ function renderWorkItemReviews(rounds) {
342
362
  ? [
343
363
  ` Frozen integrated Task heads: ${latest.taskCandidate?.projects
344
364
  .map(({ projectId, commit }) => `${projectId}@${commit}`).join(", ") ?? "unavailable"}`,
345
- ` Task-final contract: ${latest.taskFinalReviewContract?.digest ?? "global policy"}`
365
+ ` Task-final contract: ${latest.taskFinalReviewContract?.digest ?? "global policy"}`,
366
+ ...(latest.deltaRecheck === undefined
367
+ ? [` Review mode: full`]
368
+ : [
369
+ ` Review mode: delta-recheck (rechecks ${latest.deltaRecheck.previousReviewRoundId}@${latest.deltaRecheck.previousBaseCommit.slice(0, 12)})`,
370
+ ` Delta: ${latest.deltaRecheck.changedFiles.length} file(s), +${latest.deltaRecheck.addedLines}/-${latest.deltaRecheck.deletedLines}, digest ${latest.deltaRecheck.diffDigest.slice(0, 12)}`,
371
+ ...(latest.deltaRecheck.disposition === undefined
372
+ ? []
373
+ : [
374
+ ` Delta disposition: ${latest.deltaRecheck.disposition}`,
375
+ ...(latest.deltaRecheck.escalatedToReviewRoundId === undefined
376
+ ? []
377
+ : [` Escalated to: ${latest.deltaRecheck.escalatedToReviewRoundId}`])
378
+ ])
379
+ ])
346
380
  ]
347
381
  : []),
348
382
  ` Review workspace: ${latest.workspace === undefined
@@ -1,5 +1,6 @@
1
1
  import { taskNotFound, usageError } from "../errors/cliError.js";
2
2
  import { projectNextAction } from "../task/nextAction.js";
3
+ import { projectCompletionReadiness } from "../task/completionReadiness.js";
3
4
  import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
4
5
  /**
5
6
  * Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
@@ -8,6 +9,10 @@ import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
8
9
  * the judgment that remains owned by the Leader.
9
10
  * The command never mutates state; when the action is `route-review-findings`
10
11
  * it also prints the minimal repair wave for the failing Review.
12
+ *
13
+ * Issue 06: when the recommended action is `complete-task`, the output also
14
+ * carries the full `completionReadiness` blocker list so the Leader sees every
15
+ * terminalization precondition before attempting `yui task complete`.
11
16
  */
12
17
  export function runTaskNextActionCommand(args, store) {
13
18
  const usage = "Task next-action usage: yui task next-action <task> [--json].";
@@ -18,12 +23,39 @@ export function runTaskNextActionCommand(args, store) {
18
23
  }
19
24
  const taskId = positionals[0].trim();
20
25
  const data = store.transaction((reader) => {
26
+ // Issue 06: the lightweight facts drive the action on every call; the
27
+ // heavier readiness facts (full event fold, workspaces, jobs, ledger) are
28
+ // loaded only when the action is `complete-task`, keeping the per-turn
29
+ // read path cheap during execution.
21
30
  const facts = reader.readNextActionFacts(taskId);
22
31
  if (facts === null)
23
32
  throw taskNotFound(taskId);
24
33
  const action = projectNextAction(facts);
25
34
  const repairWave = repairWaveFor(action, facts);
26
- return { action, repairWave };
35
+ // Issue 12: surface pending Knowledge promotion proposals for the Task's
36
+ // bound Projects as a non-blocking advisory. The proposals are workflow
37
+ // state, not completion blockers: an Operator reviews them separately.
38
+ const knowledgeProposals = facts.task.projectBindings.flatMap((binding) => {
39
+ const project = reader.getProject(binding.projectId);
40
+ if (project === null)
41
+ return [];
42
+ return project.knowledgeProposals
43
+ .filter((proposal) => proposal.status === "pending")
44
+ .map((proposal) => ({
45
+ projectId: binding.projectId,
46
+ proposalId: proposal.id,
47
+ title: proposal.title,
48
+ sourceTaskId: proposal.source.taskId
49
+ }));
50
+ });
51
+ let completionReadiness = null;
52
+ if (action.kind === "complete-task") {
53
+ const readinessFacts = reader.readCompletionReadinessFacts(taskId);
54
+ if (readinessFacts === null)
55
+ throw taskNotFound(taskId);
56
+ completionReadiness = projectCompletionReadiness(readinessFacts);
57
+ }
58
+ return { action, repairWave, completionReadiness, knowledgeProposals };
27
59
  });
28
60
  if (asJson) {
29
61
  return {
@@ -34,7 +66,7 @@ export function runTaskNextActionCommand(args, store) {
34
66
  }
35
67
  return {
36
68
  kind: "output",
37
- output: renderNextAction(data.action, data.repairWave),
69
+ output: renderNextAction(data.action, data.repairWave, data.completionReadiness, data.knowledgeProposals),
38
70
  data
39
71
  };
40
72
  }
@@ -52,7 +84,7 @@ function repairWaveFor(action, facts) {
52
84
  return null;
53
85
  return planRepairWave(round.id, findings);
54
86
  }
55
- function renderNextAction(action, repairWave) {
87
+ function renderNextAction(action, repairWave, completionReadiness, knowledgeProposals) {
56
88
  const lines = [
57
89
  `Task: ${action.taskId}`,
58
90
  `Next action: ${action.kind}`,
@@ -91,10 +123,23 @@ function renderNextAction(action, repairWave) {
91
123
  ...action.conflicts.map((conflict) => ` ${conflict.kind}: ${conflict.id}`)
92
124
  ])
93
125
  ];
126
+ if (completionReadiness !== null) {
127
+ if (completionReadiness.ready) {
128
+ lines.push("Completion readiness: ready (no blockers)");
129
+ }
130
+ else {
131
+ lines.push(`Completion readiness: ${completionReadiness.blockers.length} blocker(s)`, ...completionReadiness.blockers.map((blocker) => ` ${blocker.code} (${blocker.ref.kind} ${blocker.ref.id}): ${blocker.reason}`
132
+ + ` — fix: ${blocker.fix}`));
133
+ }
134
+ }
94
135
  if (repairWave !== null) {
95
136
  lines.push(`Repair wave (${repairWave.openFindingCount} open finding(s), ${repairWave.groups.length} group(s)):`, ...repairWave.groups.map((group) => ` ${group.id}: findings ${group.findingIds.join(", ")}`
96
137
  + ` — paths ${group.paths.length === 0 ? "none" : group.paths.join(", ")}`
97
138
  + ` — ${group.reason}`));
98
139
  }
140
+ if (knowledgeProposals.length > 0) {
141
+ lines.push(`Knowledge proposals (non-blocking): ${knowledgeProposals.length} pending`, ...knowledgeProposals.map((proposal) => ` ${proposal.projectId}/${proposal.proposalId}: ${proposal.title}`
142
+ + ` — review: yui project knowledge proposals list ${proposal.projectId}`));
143
+ }
99
144
  return `${lines.join("\n")}\n`;
100
145
  }
@@ -0,0 +1,319 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { taskActor } from "./taskActor.js";
3
+ import { dataError, taskNotFound, usageError } from "../errors/cliError.js";
4
+ import { createTaskEvent } from "../event/taskEvent.js";
5
+ import { defaultTableWidth, renderTable } from "../output/table.js";
6
+ import { formatTimestamp } from "../output/timePresentation.js";
7
+ import { resolveProject } from "../repository/project.js";
8
+ import { createPublicationReference } from "../task/publicationReference.js";
9
+ import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
10
+ const PROVIDERS = new Set(["github", "gitlab"]);
11
+ const EXTERNAL_KINDS = new Set(["pull-request", "merge-request"]);
12
+ const STATES = new Set(["open", "merged", "closed"]);
13
+ export function runPublicationCommand(args, store, options) {
14
+ const [command, ...rest] = args;
15
+ switch (command) {
16
+ case "add": return addPublication(rest, store, options);
17
+ case "list": return listPublications(rest, store);
18
+ case "show": return showPublication(rest, store);
19
+ default:
20
+ throw usageError(command === undefined
21
+ ? "Task publication command is required."
22
+ : `Unknown command: task publication ${command}`);
23
+ }
24
+ }
25
+ function addPublication(args, store, options) {
26
+ const usage = "Task publication add usage: yui task publication add <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--supersede <publication-id>] [--merged-at <iso-timestamp>].";
27
+ const parsed = parseAddArgs(args, usage);
28
+ const task = requireTask(store, parsed.positionals[0]);
29
+ const projectReference = requiredOption(parsed.options, "--project");
30
+ const project = resolveProject(store.listProjects(), projectReference);
31
+ if (project === null)
32
+ throw usageError(`Project not found: ${projectReference}.`, usage);
33
+ if (!task.projectBindings.some((binding) => binding.projectId === project.id)) {
34
+ throw usageError(`Project is not bound to Task ${task.id}: ${projectReference}.`, usage);
35
+ }
36
+ const actor = taskActor(options.environment, task.id);
37
+ if (actor === "leader") {
38
+ throw usageError("A managed Task Leader cannot record external publication evidence; "
39
+ + "use a user or global Operator session.", usage);
40
+ }
41
+ const state = enumOption(parsed.options, "--state", STATES, usage) ?? "open";
42
+ const verification = parseVerification(parsed, usage);
43
+ const input = {
44
+ projectId: project.id,
45
+ provider: enumOption(parsed.options, "--provider", PROVIDERS, usage),
46
+ repository: requiredOption(parsed.options, "--repository"),
47
+ externalKind: enumOption(parsed.options, "--kind", EXTERNAL_KINDS, usage),
48
+ externalId: requiredOption(parsed.options, "--id"),
49
+ ...(parsed.options.has("--url") ? { externalUrl: requiredOption(parsed.options, "--url") } : {}),
50
+ ...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
51
+ ...(parsed.options.has("--source-branch")
52
+ ? { sourceBranch: requiredOption(parsed.options, "--source-branch") }
53
+ : {}),
54
+ ...(parsed.options.has("--target-branch")
55
+ ? { targetBranch: requiredOption(parsed.options, "--target-branch") }
56
+ : {}),
57
+ ...(parsed.options.has("--local-commit")
58
+ ? { localCommit: requiredOption(parsed.options, "--local-commit") }
59
+ : {}),
60
+ ...(parsed.options.has("--remote-commit")
61
+ ? { remoteCommit: requiredOption(parsed.options, "--remote-commit") }
62
+ : {}),
63
+ state,
64
+ verification,
65
+ ...(parsed.options.has("--evidence")
66
+ ? { evidence: requiredOption(parsed.options, "--evidence") }
67
+ : {}),
68
+ ...(parsed.options.has("--supersede")
69
+ ? { supersedes: requiredOption(parsed.options, "--supersede") }
70
+ : {}),
71
+ ...(parsed.options.has("--merged-at")
72
+ ? { mergedAt: requiredOption(parsed.options, "--merged-at") }
73
+ : {}),
74
+ recordedBy: actor,
75
+ source: "manual"
76
+ };
77
+ const externalKey = `${input.provider}/${input.repository}/${input.externalId}`;
78
+ const now = clock(options);
79
+ const result = store.transaction((tx) => {
80
+ const existing = tx.findPublicationReferenceByExternalKey(externalKey);
81
+ if (existing !== null) {
82
+ if (existing.taskId !== task.id) {
83
+ throw dataError(`Publication external identity already belongs to Task ${existing.taskId}: ${externalKey}.`);
84
+ }
85
+ const semantic = createPublicationReference(existing.id, task.id, input.supersedes === existing.id
86
+ ? {
87
+ projectId: input.projectId,
88
+ provider: input.provider,
89
+ repository: input.repository,
90
+ externalKind: input.externalKind,
91
+ externalId: input.externalId,
92
+ ...(input.externalUrl === undefined ? {} : { externalUrl: input.externalUrl }),
93
+ ...(input.title === undefined ? {} : { title: input.title }),
94
+ ...(input.sourceBranch === undefined ? {} : { sourceBranch: input.sourceBranch }),
95
+ ...(input.targetBranch === undefined ? {} : { targetBranch: input.targetBranch }),
96
+ ...(input.localCommit === undefined ? {} : { localCommit: input.localCommit }),
97
+ ...(input.remoteCommit === undefined ? {} : { remoteCommit: input.remoteCommit }),
98
+ state: input.state,
99
+ verification: input.verification,
100
+ ...(input.evidence === undefined ? {} : { evidence: input.evidence }),
101
+ ...(input.mergedAt === undefined ? {} : { mergedAt: input.mergedAt }),
102
+ recordedBy: input.recordedBy,
103
+ source: input.source
104
+ }
105
+ : input, new Date(existing.createdAt));
106
+ if (isDeepStrictEqual(semantic, existing)) {
107
+ return { reference: existing, idempotent: true };
108
+ }
109
+ if (input.supersedes === undefined || input.supersedes !== existing.id) {
110
+ throw usageError(`Publication ${externalKey} conflicts with current record ${existing.id}; `
111
+ + "use --supersede with that id to append corrected evidence.", usage);
112
+ }
113
+ }
114
+ else if (input.supersedes !== undefined) {
115
+ throw usageError(`Publication supersede target has a different external identity: ${input.supersedes}.`, usage);
116
+ }
117
+ const reference = createPublicationReference(tx.nextPublicationReferenceId(task.id), task.id, input, now);
118
+ tx.savePublicationReference(task.id, reference);
119
+ recordTaskEvent(tx, task.id, "publication.recorded", {
120
+ publicationId: reference.id,
121
+ provider: reference.provider,
122
+ repository: reference.repository,
123
+ externalKind: reference.externalKind,
124
+ externalId: reference.externalId,
125
+ state: reference.state,
126
+ verification: reference.verification
127
+ }, now);
128
+ return { reference, idempotent: false };
129
+ });
130
+ return output(result.idempotent
131
+ ? `Publication reference already recorded: ${result.reference.id}.\n`
132
+ : `Recorded publication ${result.reference.id} (${result.reference.provider}/${result.reference.repository}/${result.reference.externalKind}/${result.reference.externalId}) for ${result.reference.taskId}.\n`, result.reference);
133
+ }
134
+ function listPublications(args, store) {
135
+ const usage = "Task publication list usage: yui task publication list <task>.";
136
+ const parsed = parseTail(args, new Set(), usage);
137
+ exactPositionals(parsed.positionals, 1, usage);
138
+ const task = requireTask(store, parsed.positionals[0]);
139
+ const references = store.listPublicationReferences(task.id);
140
+ if (references.length === 0) {
141
+ return output("No publication references found.\n", []);
142
+ }
143
+ const rendered = `${renderTable(`Publication references: ${task.id}`, [
144
+ { header: "ID", minWidth: 12, maxWidth: 22 },
145
+ { header: "External", minWidth: 16, maxWidth: 40 },
146
+ { header: "Title", minWidth: 16, maxWidth: 40 },
147
+ { header: "State", minWidth: 7, maxWidth: 10 },
148
+ { header: "Verification", minWidth: 11, maxWidth: 14 },
149
+ { header: "Lineage", minWidth: 18, maxWidth: 58 },
150
+ { header: "Supersedes", minWidth: 12, maxWidth: 22 }
151
+ ], references.map((reference) => [
152
+ reference.id,
153
+ `${reference.provider}/${reference.repository}/${reference.externalId}`,
154
+ reference.title ?? "-",
155
+ reference.state,
156
+ reference.verification,
157
+ lineageSummary(reference),
158
+ reference.supersedes ?? "-"
159
+ ]), defaultTableWidth())}\n`;
160
+ return output(rendered, references);
161
+ }
162
+ function showPublication(args, store) {
163
+ const usage = "Task publication show usage: yui task publication show (<task>/<publication-id> | <task> <publication-id>).";
164
+ const parsed = parseTail(args, new Set(), usage);
165
+ if (parsed.positionals.length !== 1 && parsed.positionals.length !== 2) {
166
+ throw usageError(usage);
167
+ }
168
+ const referenceId = parsed.positionals.length === 1
169
+ ? resolveTaskRecordReference(parsed.positionals[0], {
170
+ kind: "publicationReference",
171
+ label: "Publication reference"
172
+ })
173
+ : resolveTaskRecordReference(`${parsed.positionals[0]}/${parsed.positionals[1]}`, { kind: "publicationReference", label: "Publication reference" });
174
+ requireTask(store, referenceId.taskId);
175
+ const reference = store.getPublicationReference(referenceId.taskId, referenceId.localId);
176
+ if (reference === null) {
177
+ throw dataError(`Publication reference not found: ${referenceId.taskId}/${referenceId.localId}.`);
178
+ }
179
+ return output(renderPublication(reference, store.getConfig().timeZone), reference);
180
+ }
181
+ function renderPublication(reference, timeZone) {
182
+ return [
183
+ `Publication: ${reference.id}`,
184
+ `Task: ${reference.taskId}`,
185
+ `Project: ${reference.projectId}`,
186
+ `External: ${reference.provider}/${reference.repository}/${reference.externalId} (${reference.externalKind})`,
187
+ ...(reference.externalUrl === undefined ? [] : [`URL: ${reference.externalUrl}`]),
188
+ ...(reference.title === undefined ? [] : [`Title: ${reference.title}`]),
189
+ ...(reference.sourceBranch === undefined && reference.targetBranch === undefined
190
+ ? []
191
+ : [`Branches: ${reference.sourceBranch ?? "unknown"} -> ${reference.targetBranch ?? "unknown"}`]),
192
+ `State: ${reference.state}`,
193
+ `Verification: ${reference.verification}`,
194
+ `Lineage: ${reference.localCommit ?? "unknown"} -> ${reference.externalId} -> ${reference.remoteCommit ?? "unknown"}`,
195
+ ...(reference.mergedAt === undefined
196
+ ? []
197
+ : [`Merged: ${formatTimestamp(reference.mergedAt, timeZone)}`]),
198
+ ...(reference.evidence === undefined ? [] : [`Evidence: ${reference.evidence}`]),
199
+ ...(reference.supersedes === undefined ? [] : [`Supersedes: ${reference.supersedes}`]),
200
+ `Recorded by: ${reference.recordedBy}`,
201
+ `Source: ${reference.source}`,
202
+ `Created: ${formatTimestamp(reference.createdAt, timeZone)}`
203
+ ].join("\n").concat("\n");
204
+ }
205
+ function lineageSummary(reference) {
206
+ const local = reference.localCommit ?? "unknown";
207
+ const remote = reference.remoteCommit ?? "unknown";
208
+ return `${local.slice(0, 12)} -> ${reference.externalId} -> ${remote.slice(0, 12)}`;
209
+ }
210
+ function parseVerification(parsed, usage) {
211
+ if (parsed.flags.has("--reported") && parsed.flags.has("--verified")) {
212
+ throw usageError("--reported and --verified are mutually exclusive.", usage);
213
+ }
214
+ return parsed.flags.has("--verified") ? "verified" : "reported";
215
+ }
216
+ function enumOption(options, name, allowed, usage) {
217
+ if (!options.has(name))
218
+ return undefined;
219
+ const value = requiredOption(options, name);
220
+ if (!allowed.has(value)) {
221
+ throw usageError(`${name} must be one of: ${[...allowed].join(", ")}.`, usage);
222
+ }
223
+ return value;
224
+ }
225
+ function requireTask(store, taskId) {
226
+ const id = requiredText(taskId, "Task id");
227
+ const task = store.getTask(id);
228
+ if (task === null)
229
+ throw taskNotFound(id);
230
+ return task;
231
+ }
232
+ function recordTaskEvent(store, taskId, type, payload, now) {
233
+ store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now));
234
+ }
235
+ function requiredOption(options, name) {
236
+ return requiredText(options.get(name), name);
237
+ }
238
+ function requiredText(value, label) {
239
+ const normalized = value?.trim();
240
+ if (normalized === undefined || normalized.length === 0) {
241
+ throw usageError(`${label} is required.`);
242
+ }
243
+ return normalized;
244
+ }
245
+ function clock(options) {
246
+ return options.now?.() ?? new Date();
247
+ }
248
+ function output(value, data) {
249
+ return data === undefined
250
+ ? { kind: "output", output: value }
251
+ : { kind: "output", output: value, data };
252
+ }
253
+ function exactPositionals(values, count, usage) {
254
+ if (values.length !== count || values.some((value) => value.trim().length === 0)) {
255
+ throw usageError(usage);
256
+ }
257
+ }
258
+ function parseAddArgs(args, usage) {
259
+ const valueOptions = new Set([
260
+ "--project", "--provider", "--repository", "--kind", "--id", "--url",
261
+ "--title", "--source-branch", "--target-branch", "--local-commit",
262
+ "--remote-commit", "--state", "--evidence", "--supersede", "--merged-at"
263
+ ]);
264
+ const flags = new Set(["--reported", "--verified"]);
265
+ const positionals = [];
266
+ const options = new Map();
267
+ const seenFlags = new Set();
268
+ for (let index = 0; index < args.length; index += 1) {
269
+ const value = args[index];
270
+ if (!value.startsWith("--")) {
271
+ positionals.push(value);
272
+ continue;
273
+ }
274
+ if (flags.has(value)) {
275
+ if (seenFlags.has(value)) {
276
+ throw usageError(`Flag may only be specified once: ${value}.`, usage);
277
+ }
278
+ seenFlags.add(value);
279
+ continue;
280
+ }
281
+ if (!valueOptions.has(value)) {
282
+ throw usageError(`Unsupported option: ${value}.`, usage);
283
+ }
284
+ if (options.has(value)) {
285
+ throw usageError(`Option may only be specified once: ${value}.`, usage);
286
+ }
287
+ const optionValue = args[index + 1];
288
+ if (optionValue === undefined || optionValue.startsWith("--")) {
289
+ throw usageError(`${value} is required.`, usage);
290
+ }
291
+ options.set(value, optionValue);
292
+ index += 1;
293
+ }
294
+ return { positionals, options, flags: seenFlags };
295
+ }
296
+ function parseTail(args, valueOptions, usage) {
297
+ const positionals = [];
298
+ const options = new Map();
299
+ for (let index = 0; index < args.length; index += 1) {
300
+ const value = args[index];
301
+ if (!value.startsWith("--")) {
302
+ positionals.push(value);
303
+ continue;
304
+ }
305
+ if (!valueOptions.has(value)) {
306
+ throw usageError(`Unsupported option: ${value}.`, usage);
307
+ }
308
+ if (options.has(value)) {
309
+ throw usageError(`Option may only be specified once: ${value}.`, usage);
310
+ }
311
+ const optionValue = args[index + 1];
312
+ if (optionValue === undefined || optionValue.startsWith("--")) {
313
+ throw usageError(`${value} is required.`, usage);
314
+ }
315
+ options.set(value, optionValue);
316
+ index += 1;
317
+ }
318
+ return { positionals, options };
319
+ }