@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
@@ -1,8 +1,8 @@
1
- import { leaderNextActionMode } from "../config/yuiConfig.js";
1
+ import { resolveLeaderNextActionMode } from "../config/yuiConfig.js";
2
2
  import { usageError } from "../errors/cliError.js";
3
3
  import { detectDeliveryDuplicates, evaluateDeliveryGuard, evaluateSemanticBudget, formatDeliveryDuplicate } from "../task/deliveryGuard.js";
4
4
  export function runDeliveryGuardPreflight(store, taskId, intent, options = {}) {
5
- const mode = leaderNextActionMode(store.getConfig().leaderNextActionMode, options.environment);
5
+ const mode = resolveLeaderNextActionMode(store.getConfig().leaderNextActionMode);
6
6
  if (mode === "display")
7
7
  return { warnings: [] };
8
8
  const facts = store.readNextActionFacts(taskId);
@@ -104,6 +104,15 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
104
104
  { header: "Count", minWidth: 5, maxWidth: 8 }
105
105
  ], faultRows.map(([name, count]) => [name, String(count)]), width));
106
106
  }
107
+ if (runs.launchFailures.total > 0) {
108
+ const phaseRows = Object.entries(runs.launchFailures.byPhase)
109
+ .filter(([, count]) => count > 0)
110
+ .sort((left, right) => right[1] - left[1]);
111
+ const kindRows = Object.entries(runs.launchFailures.byKind)
112
+ .filter(([, count]) => count > 0)
113
+ .sort((left, right) => right[1] - left[1]);
114
+ lines.push("", `Launch failures: ${runs.launchFailures.total}`, `By phase: ${phaseRows.map(([name, count]) => `${name}=${count}`).join(" · ")}`, `By kind: ${kindRows.map(([name, count]) => `${name}=${count}`).join(" · ")}`);
115
+ }
107
116
  }
108
117
  else {
109
118
  lines.push("", ...sectionError("runs", report));
@@ -111,7 +120,10 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
111
120
  if (report.wakes.status === "ok" && report.wakes.data !== undefined) {
112
121
  const wakes = report.wakes.data;
113
122
  lines.push("", `Leader wakes: ${wakes.leaderRuns} runs · ${wakes.withWakeReasons} with reasons · ${wakes.orphanWakes} orphan wakes (${wakes.orphanYieldOnly} yield-only)`);
114
- if (wakes.suppressedWakes.status === "unsupported") {
123
+ if (wakes.suppressedWakes.status === "ok") {
124
+ lines.push(`Suppressed wakes: ${wakes.suppressedWakes.data ?? 0} (scheduler single-flight, not failed Runs)`);
125
+ }
126
+ else if (wakes.suppressedWakes.status === "unsupported") {
115
127
  lines.push("Suppressed wakes: unsupported (no quiescence producer in this build)");
116
128
  }
117
129
  const reasonRows = Object.entries(wakes.byReason).slice(0, 8);
@@ -127,14 +139,24 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
127
139
  }
128
140
  if (report.sessions.status === "ok" && report.sessions.data !== undefined) {
129
141
  const sessions = report.sessions.data;
130
- lines.push("", `Sessions: ${sessions.generations} generations · ${sessions.broken} broken · ${sessions.stopped} stopped · ${sessions.other} other`, `Resets: ${sessions.resets} · lifecycle events ${sessions.lifecycleEvents} · stop failures ${sessions.stopFailures}`);
142
+ lines.push("", `Sessions: ${sessions.generations} generations · ${sessions.broken} broken · ${sessions.stopped} stopped · ${sessions.other} other`, `Resets: ${sessions.resets} · lifecycle events ${sessions.lifecycleEvents} · stop failures ${sessions.stopFailures}`, `Terminal by Run relation: ${sessions.terminalByRunRelation.postRunYielded} post-run-yielded`
143
+ + ` · ${sessions.terminalByRunRelation.runFailed} run-failed`
144
+ + ` · ${sessions.terminalByRunRelation.activeRun} active-run`
145
+ + ` · ${sessions.terminalByRunRelation.noRun} no-run`);
131
146
  }
132
147
  else {
133
148
  lines.push("", ...sectionError("sessions", report));
134
149
  }
135
150
  if (report.reviews.status === "ok" && report.reviews.data !== undefined) {
136
151
  const reviews = report.reviews.data;
137
- lines.push("", `Review rounds: ${reviews.total} total · ${reviews.completed} completed · ${reviews.failed} failed`, `Execution failures: ${reviews.infraFailed} · semantic negatives: ${reviews.semanticNegative}`);
152
+ lines.push("", `Review rounds: ${reviews.total} total · ${reviews.completed} completed · ${reviews.failed} failed`, `Execution failures: ${reviews.infraFailed} · semantic negatives: ${reviews.semanticNegative}`, ...(reviews.deltaRechecks.total === 0
153
+ ? []
154
+ : [
155
+ `Delta-rechecks: ${reviews.deltaRechecks.total} total · `
156
+ + `${reviews.deltaRechecks.equivalentAndAccepted} accepted · `
157
+ + `${reviews.deltaRechecks.finding} finding · `
158
+ + `${reviews.deltaRechecks.requiresFullReview} escalated to full Review`
159
+ ]));
138
160
  }
139
161
  else {
140
162
  lines.push("", ...sectionError("reviews", report));
@@ -146,6 +168,13 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
146
168
  else {
147
169
  lines.push("", ...sectionError("integrations", report));
148
170
  }
171
+ if (report.publications.status === "ok" && report.publications.data !== undefined) {
172
+ const publications = report.publications.data;
173
+ lines.push("", `Publication references: ${publications.total} total · ${publications.merged} merged · ${publications.verified} verified · ${publications.open} open · ${publications.closed} closed · ${publications.superseded} superseded`);
174
+ }
175
+ else {
176
+ lines.push("", ...sectionError("publications", report));
177
+ }
149
178
  if (report.events.status === "ok" && report.events.data !== undefined) {
150
179
  const events = report.events.data;
151
180
  lines.push("", `Events: ${events.total} total · ${events.progressEvents} provider-progress (${(events.progressShare * 100).toFixed(1)}%) · ${events.semanticEvents} semantic · ${events.obsoleteEvents} obsolete`, `Messages: ${events.messages}`);
@@ -153,6 +182,30 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
153
182
  else {
154
183
  lines.push("", ...sectionError("events", report));
155
184
  }
185
+ if (report.providerRetries.status === "ok" && report.providerRetries.data !== undefined) {
186
+ const retries = report.providerRetries.data;
187
+ if (retries.total > 0) {
188
+ lines.push("", `Provider retries: ${retries.total} run(s) retried in place · ${retries.terminal} terminal`);
189
+ lines.push(renderTable("Provider retry lineages", [
190
+ { header: "Task", minWidth: 8, maxWidth: 14 },
191
+ { header: "Run", minWidth: 14, maxWidth: 24 },
192
+ { header: "Role", minWidth: 8, maxWidth: 12 },
193
+ { header: "Attempts", minWidth: 8, maxWidth: 10 },
194
+ { header: "Error class", minWidth: 14, maxWidth: 24 },
195
+ { header: "Decision", minWidth: 14, maxWidth: 26 }
196
+ ], retries.entries.map((entry) => [
197
+ entry.taskId,
198
+ entry.runId,
199
+ entry.roleName,
200
+ String(entry.attempts),
201
+ entry.errorClass,
202
+ entry.decision
203
+ ]), width));
204
+ }
205
+ }
206
+ else {
207
+ lines.push("", ...sectionError("providerRetries", report));
208
+ }
156
209
  if (report.workItems.status === "ok" && report.workItems.data !== undefined) {
157
210
  const items = report.workItems.data;
158
211
  lines.push("", `Work items: ${items.total} total · ${items.completed} completed · ${items.retired} retired`);
@@ -5,7 +5,8 @@ import { usageError } from "../errors/cliError.js";
5
5
  import { defaultTableWidth, renderTable } from "../output/table.js";
6
6
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
7
7
  import { acquireProjectMaintenanceLock } from "../repository/projectMaintenanceLock.js";
8
- import { addProjectKnowledge, createProject, managedProjectPath, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
8
+ import { addProjectKnowledge, addKnowledgeProposal, decideKnowledgeProposal, createProject, findKnowledgeProposal, findKnowledgeProposalByFingerprint, knowledgeEvidenceDigest, knowledgeProposalFingerprint, managedProjectPath, planKnowledgeAcceptance, retireProjectKnowledge, resolveProject, updateProjectKnowledge, updateProjectMetadata, validateProject, validateProjectName } from "../repository/project.js";
9
+ import { projectActor } from "./taskActor.js";
9
10
  export async function runProjectCommand(args, store, options = {}) {
10
11
  const [command, ...rest] = args;
11
12
  if (command === "add" || command === "clone") {
@@ -521,7 +522,8 @@ function showProject(args, store) {
521
522
  ...(project.remoteUrl === undefined ? [] : [`Remote: ${project.remoteUrl}`]),
522
523
  `Stable: ${project.stableBranch}`,
523
524
  `Development: ${project.developmentBranch}`,
524
- `Knowledge: ${project.knowledge.filter(({ status }) => status === "active").length} active`
525
+ `Knowledge: ${project.knowledge.filter(({ status }) => status === "active").length} active`,
526
+ `Knowledge proposals: ${project.knowledgeProposals.filter(({ status }) => status === "pending").length} pending`
525
527
  ].join("\n").concat("\n");
526
528
  }
527
529
  function projectKnowledge(args, store, options) {
@@ -531,6 +533,7 @@ function projectKnowledge(args, store, options) {
531
533
  const parsed = parseSingleOption(rest, "--body", usage);
532
534
  if (parsed.positionals.length !== 2)
533
535
  throw usageError(usage);
536
+ assertKnowledgeOperator(options, "add");
534
537
  const added = store.transaction((tx) => {
535
538
  const project = requireProject(tx, parsed.positionals[0]);
536
539
  const id = nextKnowledgeId(project);
@@ -584,6 +587,21 @@ function projectKnowledge(args, store, options) {
584
587
  `Project: ${project.id}`,
585
588
  `Title: ${entry.title}`,
586
589
  `Status: ${entry.status}`,
590
+ ...(entry.provenance === undefined ? [] : [
591
+ `Source: ${entry.provenance.taskId}`
592
+ + `${entry.provenance.decisionId === undefined ? "" : `/${entry.provenance.decisionId}`}`
593
+ + `${entry.provenance.milestoneId === undefined ? "" : `/${entry.provenance.milestoneId}`}`
594
+ + `${entry.provenance.commitSha === undefined ? "" : `@${entry.provenance.commitSha}`}`,
595
+ ...(entry.provenance.evidenceDigest === undefined
596
+ ? []
597
+ : [`Evidence digest: ${entry.provenance.evidenceDigest}`]),
598
+ ...(entry.provenance.proposalId === undefined
599
+ ? []
600
+ : [`Proposal: ${entry.provenance.proposalId}`]),
601
+ ...(entry.provenance.promotedBy === undefined
602
+ ? []
603
+ : [`Promoted by: ${entry.provenance.promotedBy} at ${entry.provenance.promotedAt ?? "?"}`])
604
+ ]),
587
605
  "",
588
606
  entry.body
589
607
  ].join("\n").concat("\n"),
@@ -594,6 +612,7 @@ function projectKnowledge(args, store, options) {
594
612
  if (command === "update") {
595
613
  const usage = "Project knowledge update usage: yui project knowledge update <project> <knowledge> [--title <text>] [--body <text>].";
596
614
  const parsed = parseKnowledgeUpdateArguments(rest, usage);
615
+ assertKnowledgeOperator(options, "update");
597
616
  const updated = store.transaction((tx) => {
598
617
  const project = requireProject(tx, parsed.project);
599
618
  const next = updateProjectKnowledge(project, parsed.id, {
@@ -613,6 +632,7 @@ function projectKnowledge(args, store, options) {
613
632
  if (rest.length !== 2) {
614
633
  throw usageError("Project knowledge retire usage: yui project knowledge retire <project> <knowledge>.");
615
634
  }
635
+ assertKnowledgeOperator(options, "retire");
616
636
  const updated = store.transaction((tx) => {
617
637
  const project = requireProject(tx, rest[0]);
618
638
  const next = retireProjectKnowledge(project, rest[1], (options.now ?? (() => new Date()))());
@@ -625,6 +645,24 @@ function projectKnowledge(args, store, options) {
625
645
  knowledgeId: rest[1]
626
646
  };
627
647
  }
648
+ if (command === "propose") {
649
+ return proposeKnowledge(rest, store, options);
650
+ }
651
+ if (command === "proposals") {
652
+ const [sub, ...subRest] = rest;
653
+ if (sub === "list")
654
+ return listKnowledgeProposals(subRest, store);
655
+ if (sub === "show")
656
+ return showKnowledgeProposal(subRest, store);
657
+ throw usageError("Project knowledge proposals usage: yui project knowledge proposals list <project>"
658
+ + " [--status pending|accepted|rejected] [--all] | yui project knowledge proposals show <project> <proposal>.");
659
+ }
660
+ if (command === "accept") {
661
+ return acceptKnowledgeProposal(rest, store, options);
662
+ }
663
+ if (command === "reject") {
664
+ return rejectKnowledgeProposal(rest, store, options);
665
+ }
628
666
  throw usageError(command === undefined
629
667
  ? "Project knowledge command is required."
630
668
  : `Unknown command: project knowledge ${command}`);
@@ -643,6 +681,470 @@ function nextKnowledgeId(project) {
643
681
  return id;
644
682
  }
645
683
  }
684
+ function nextProposalId(project) {
685
+ const used = new Set(project.knowledgeProposals.map(({ id }) => id));
686
+ for (let index = 1;; index += 1) {
687
+ const id = `proposal-${index}`;
688
+ if (!used.has(id))
689
+ return id;
690
+ }
691
+ }
692
+ /**
693
+ * Project Knowledge is an Operator authority. A managed Task Session (Leader,
694
+ * Reviewer, Worker) must use `propose` and let an Operator accept; it can
695
+ * never write the authoritative list directly.
696
+ */
697
+ function assertKnowledgeOperator(options, action) {
698
+ const actor = projectActor(options.environment);
699
+ if (actor === "agent") {
700
+ throw usageError(`Project Knowledge is an Operator authority; a managed Task Session cannot ${action} it.`
701
+ + " Propose a candidate with `yui project knowledge propose` instead.");
702
+ }
703
+ }
704
+ const PROPOSE_OPTIONS = [
705
+ "--title", "--body", "--task", "--decision", "--milestone",
706
+ "--commit", "--scope", "--expires-when", "--supersedes"
707
+ ];
708
+ function parseKnowledgeProposalOptions(args, usage) {
709
+ const values = {};
710
+ const positionals = [];
711
+ for (let index = 0; index < args.length; index += 1) {
712
+ const value = args[index];
713
+ if (PROPOSE_OPTIONS.includes(value)) {
714
+ if (values[value] !== undefined) {
715
+ throw usageError(`${value} may only be provided once. ${usage}`);
716
+ }
717
+ const next = args[index + 1];
718
+ if (next === undefined || next.startsWith("--")) {
719
+ throw usageError(`${value} is required. ${usage}`);
720
+ }
721
+ values[value] = requireText(next, value);
722
+ index += 1;
723
+ }
724
+ else if (value.startsWith("--")) {
725
+ throw usageError(`Unknown option: ${value}. ${usage}`);
726
+ }
727
+ else {
728
+ positionals.push(value);
729
+ }
730
+ }
731
+ if (positionals.length !== 1)
732
+ throw usageError(usage);
733
+ if (values["--title"] === undefined || values["--body"] === undefined || values["--task"] === undefined) {
734
+ throw usageError(usage);
735
+ }
736
+ return {
737
+ project: positionals[0],
738
+ title: values["--title"],
739
+ body: values["--body"],
740
+ task: values["--task"],
741
+ ...(values["--decision"] === undefined ? {} : { decision: values["--decision"] }),
742
+ ...(values["--milestone"] === undefined ? {} : { milestone: values["--milestone"] }),
743
+ ...(values["--commit"] === undefined ? {} : { commit: values["--commit"] }),
744
+ ...(values["--scope"] === undefined ? {} : { scope: values["--scope"] }),
745
+ ...(values["--expires-when"] === undefined ? {} : { expiresWhen: values["--expires-when"] }),
746
+ ...(values["--supersedes"] === undefined ? {} : { supersedes: values["--supersedes"] })
747
+ };
748
+ }
749
+ /**
750
+ * Leader-proposed promotion candidate. The proposal is workflow state, not
751
+ * Knowledge: it never appears in `project knowledge list/show` until an
752
+ * Operator accepts it. A managed Task Session may propose only for its own
753
+ * Task. The same candidate (source identity + title + body) is deduplicated to
754
+ * the existing pending proposal instead of creating a second copy.
755
+ */
756
+ function proposeKnowledge(args, store, options) {
757
+ const usage = "Project knowledge propose usage: yui project knowledge propose <project>"
758
+ + " --title <text> --body <text> --task <task> [--decision <id>] [--milestone <id>]"
759
+ + " [--commit <sha>] [--scope <text>] [--expires-when <text>] [--supersedes <knowledge-id>].";
760
+ const parsed = parseKnowledgeProposalOptions(args, usage);
761
+ const actor = projectActor(options.environment);
762
+ if (actor === "agent" && options.environment?.YUI_TASK_ID !== parsed.task) {
763
+ throw usageError("A managed Task Session may propose Knowledge only for its own Task.");
764
+ }
765
+ if (actor === "agent"
766
+ && options.environment?.YUI_ROLE !== "leader"
767
+ && options.environment?.YUI_ROLE !== "reviewer") {
768
+ throw usageError("Only a Leader or Reviewer Session may propose Knowledge promotion.");
769
+ }
770
+ const proposedBy = actor === "agent"
771
+ ? (options.environment?.YUI_ROLE === "reviewer" ? "reviewer" : "leader")
772
+ : actor;
773
+ const now = (options.now ?? (() => new Date()))();
774
+ const result = store.transaction((tx) => {
775
+ const project = requireProject(tx, parsed.project);
776
+ const source = requireProposalEvidence(tx, parsed);
777
+ const fingerprint = knowledgeProposalFingerprint({
778
+ projectId: project.id,
779
+ source,
780
+ title: parsed.title,
781
+ body: parsed.body
782
+ });
783
+ const pending = findKnowledgeProposalByFingerprint(project, fingerprint, "pending");
784
+ if (pending !== null) {
785
+ return {
786
+ projectId: project.id,
787
+ proposalId: pending.id,
788
+ deduped: true,
789
+ alreadyAccepted: false
790
+ };
791
+ }
792
+ const accepted = findKnowledgeProposalByFingerprint(project, fingerprint, "accepted");
793
+ if (accepted !== null) {
794
+ return {
795
+ projectId: project.id,
796
+ proposalId: accepted.id,
797
+ knowledgeId: accepted.knowledgeId,
798
+ deduped: true,
799
+ alreadyAccepted: true
800
+ };
801
+ }
802
+ const proposal = {
803
+ schemaVersion: 1,
804
+ id: nextProposalId(project),
805
+ projectId: project.id,
806
+ title: parsed.title,
807
+ body: parsed.body,
808
+ status: "pending",
809
+ source,
810
+ ...(parsed.scope === undefined ? {} : { scope: parsed.scope }),
811
+ ...(parsed.expiresWhen === undefined ? {} : { expiresWhen: parsed.expiresWhen }),
812
+ ...(parsed.supersedes === undefined ? {} : { supersedesKnowledgeId: parsed.supersedes }),
813
+ fingerprint,
814
+ ...(source.evidenceDigest === undefined ? {} : { evidenceDigest: source.evidenceDigest }),
815
+ proposedBy,
816
+ proposedAt: now.toISOString(),
817
+ updatedAt: now.toISOString()
818
+ };
819
+ tx.saveProject(addKnowledgeProposal(project, proposal));
820
+ return {
821
+ projectId: project.id,
822
+ proposalId: proposal.id,
823
+ deduped: false,
824
+ alreadyAccepted: false
825
+ };
826
+ });
827
+ const lines = [
828
+ result.deduped
829
+ ? `Knowledge proposal ${result.proposalId} already exists for ${result.projectId}`
830
+ : `Proposed knowledge ${result.proposalId} for ${result.projectId}`,
831
+ result.alreadyAccepted
832
+ ? `Already accepted as knowledge ${result.knowledgeId ?? "?"}`
833
+ : "Awaiting Operator decision: yui project knowledge proposals list "
834
+ + `${result.projectId} / yui project knowledge accept ${result.projectId} ${result.proposalId}`
835
+ ];
836
+ return {
837
+ output: `${lines.join("\n")}\n`,
838
+ projectId: result.projectId,
839
+ proposalId: result.proposalId,
840
+ ...(result.knowledgeId === undefined ? {} : { knowledgeId: result.knowledgeId })
841
+ };
842
+ }
843
+ /**
844
+ * Verify the cited Task/Decision/Milestone exist and capture the evidence
845
+ * digest at proposal time, so a later Agent can verify the source record.
846
+ */
847
+ function requireProposalEvidence(store, parsed) {
848
+ if (store.getTask === undefined) {
849
+ throw usageError("Knowledge promotion requires Task evidence, which this store does not provide.");
850
+ }
851
+ const task = store.getTask(parsed.task);
852
+ if (task === null)
853
+ throw usageError(`Task not found: ${parsed.task}.`);
854
+ const source = {
855
+ taskId: task.id,
856
+ ...(parsed.decision === undefined ? {} : { decisionId: parsed.decision }),
857
+ ...(parsed.milestone === undefined ? {} : { milestoneId: parsed.milestone }),
858
+ ...(parsed.commit === undefined ? {} : { commitSha: parsed.commit })
859
+ };
860
+ let evidence;
861
+ if (parsed.decision !== undefined) {
862
+ if (store.getDecision === undefined) {
863
+ throw usageError("Knowledge promotion requires Decision evidence, which this store does not provide.");
864
+ }
865
+ const decision = store.getDecision(task.id, parsed.decision);
866
+ if (decision === null) {
867
+ throw usageError(`Decision not found: ${parsed.decision} in Task ${task.id}.`);
868
+ }
869
+ evidence = `${decision.title}\n${decision.rationale}`;
870
+ }
871
+ else if (parsed.milestone !== undefined) {
872
+ if (store.getMilestone === undefined) {
873
+ throw usageError("Knowledge promotion requires Milestone evidence, which this store does not provide.");
874
+ }
875
+ const milestone = store.getMilestone(task.id, parsed.milestone);
876
+ if (milestone === null) {
877
+ throw usageError(`Milestone not found: ${parsed.milestone} in Task ${task.id}.`);
878
+ }
879
+ evidence = `${milestone.title}\n${milestone.summary}`;
880
+ }
881
+ else {
882
+ evidence = task.completionSummary ?? task.title;
883
+ }
884
+ return { ...source, evidenceDigest: knowledgeEvidenceDigest(evidence) };
885
+ }
886
+ function listKnowledgeProposals(args, store) {
887
+ const usage = "Project knowledge proposals list usage: yui project knowledge proposals list <project>"
888
+ + " [--status pending|accepted|rejected] [--all].";
889
+ const positionals = [];
890
+ let status = "pending";
891
+ let sawAll = false;
892
+ let sawStatus = false;
893
+ for (let index = 0; index < args.length; index += 1) {
894
+ const value = args[index];
895
+ if (value === "--all") {
896
+ if (sawStatus)
897
+ throw usageError(`--all and --status are mutually exclusive. ${usage}`);
898
+ sawAll = true;
899
+ status = undefined;
900
+ }
901
+ else if (value === "--status") {
902
+ if (sawAll)
903
+ throw usageError(`--all and --status are mutually exclusive. ${usage}`);
904
+ sawStatus = true;
905
+ const next = args[index + 1];
906
+ if (next === undefined || next.startsWith("--"))
907
+ throw usageError(usage);
908
+ if (next !== "pending" && next !== "accepted" && next !== "rejected")
909
+ throw usageError(usage);
910
+ status = next;
911
+ index += 1;
912
+ }
913
+ else if (value.startsWith("--")) {
914
+ throw usageError(`Unknown option: ${value}. ${usage}`);
915
+ }
916
+ else {
917
+ positionals.push(value);
918
+ }
919
+ }
920
+ if (positionals.length !== 1)
921
+ throw usageError(usage);
922
+ const project = requireProject(store, positionals[0]);
923
+ const entries = project.knowledgeProposals
924
+ .filter((entry) => status === undefined || entry.status === status)
925
+ .slice()
926
+ .sort((a, b) => a.id.localeCompare(b.id));
927
+ if (entries.length === 0) {
928
+ return {
929
+ output: "No knowledge proposals found.\n",
930
+ projectId: project.id,
931
+ proposals: []
932
+ };
933
+ }
934
+ return {
935
+ output: `${renderTable(`Knowledge proposals: ${project.name}`, [
936
+ { header: "Proposal", minWidth: 9, maxWidth: 24 },
937
+ { header: "Title", minWidth: 8, maxWidth: 48 },
938
+ { header: "Status", minWidth: 7, maxWidth: 10 },
939
+ { header: "Source", minWidth: 8, maxWidth: 32 },
940
+ { header: "Knowledge", minWidth: 9, maxWidth: 24 }
941
+ ], entries.map((entry) => [
942
+ entry.id,
943
+ entry.title,
944
+ entry.status,
945
+ `${entry.source.taskId}`
946
+ + `${entry.source.decisionId === undefined ? "" : `/${entry.source.decisionId}`}`
947
+ + `${entry.source.milestoneId === undefined ? "" : `/${entry.source.milestoneId}`}`,
948
+ entry.knowledgeId ?? "-"
949
+ ]), defaultTableWidth())}\n`,
950
+ projectId: project.id,
951
+ proposals: entries
952
+ };
953
+ }
954
+ function showKnowledgeProposal(args, store) {
955
+ const usage = "Project knowledge proposals show usage: yui project knowledge proposals show <project> <proposal>.";
956
+ if (args.length !== 2)
957
+ throw usageError(usage);
958
+ const project = requireProject(store, args[0]);
959
+ const proposal = findKnowledgeProposal(project, args[1]);
960
+ if (proposal === null) {
961
+ throw usageError(`Knowledge proposal not found: ${args[1]}.`);
962
+ }
963
+ const lines = [
964
+ `Proposal: ${proposal.id}`,
965
+ `Project: ${project.id}`,
966
+ `Title: ${proposal.title}`,
967
+ `Status: ${proposal.status}`,
968
+ `Source: ${proposal.source.taskId}`
969
+ + `${proposal.source.decisionId === undefined ? "" : `/${proposal.source.decisionId}`}`
970
+ + `${proposal.source.milestoneId === undefined ? "" : `/${proposal.source.milestoneId}`}`
971
+ + `${proposal.source.commitSha === undefined ? "" : `@${proposal.source.commitSha}`}`,
972
+ `Proposed by: ${proposal.proposedBy} at ${proposal.proposedAt}`,
973
+ ...(proposal.scope === undefined ? [] : [`Scope: ${proposal.scope}`]),
974
+ ...(proposal.expiresWhen === undefined ? [] : [`Expires when: ${proposal.expiresWhen}`]),
975
+ ...(proposal.supersedesKnowledgeId === undefined
976
+ ? []
977
+ : [`Supersedes: ${proposal.supersedesKnowledgeId}`]),
978
+ ...(proposal.evidenceDigest === undefined ? [] : [`Evidence digest: ${proposal.evidenceDigest}`]),
979
+ ...(proposal.decidedBy === undefined ? [] : [`Decided by: ${proposal.decidedBy} at ${proposal.decidedAt ?? "?"}`]),
980
+ ...(proposal.decisionReason === undefined ? [] : [`Decision reason: ${proposal.decisionReason}`]),
981
+ ...(proposal.knowledgeId === undefined ? [] : [`Knowledge: ${proposal.knowledgeId}`]),
982
+ "",
983
+ proposal.body
984
+ ];
985
+ return {
986
+ output: `${lines.join("\n")}\n`,
987
+ projectId: project.id,
988
+ proposalId: proposal.id,
989
+ proposals: [proposal]
990
+ };
991
+ }
992
+ /**
993
+ * Operator acceptance. The pure {@link planKnowledgeAcceptance} policy decides
994
+ * create/update/duplicate/conflict; a conflict fails closed so a candidate can
995
+ * never silently overwrite an existing conclusion. The accepted Knowledge
996
+ * entry carries the source provenance and fingerprint, and the proposal record
997
+ * retains the decision for history.
998
+ */
999
+ function acceptKnowledgeProposal(args, store, options) {
1000
+ const usage = "Project knowledge accept usage: yui project knowledge accept <project> <proposal>"
1001
+ + " [--update <knowledge-id>].";
1002
+ const parsed = parseAcceptArguments(args, usage);
1003
+ assertKnowledgeOperator(options, "accept");
1004
+ const actor = projectActor(options.environment);
1005
+ const decidedBy = actor === "operator" ? "operator" : "user";
1006
+ const now = (options.now ?? (() => new Date()))();
1007
+ const accepted = store.transaction((tx) => {
1008
+ const project = requireProject(tx, parsed.project);
1009
+ const proposal = findKnowledgeProposal(project, parsed.proposal);
1010
+ if (proposal === null) {
1011
+ throw usageError(`Knowledge proposal not found: ${parsed.proposal}.`);
1012
+ }
1013
+ if (proposal.status !== "pending") {
1014
+ throw usageError(`Knowledge proposal is already ${proposal.status}: ${proposal.id}.`);
1015
+ }
1016
+ let next = project;
1017
+ let knowledgeId;
1018
+ let duplicate = false;
1019
+ if (parsed.update !== undefined) {
1020
+ const target = next.knowledge.find((entry) => entry.id === parsed.update);
1021
+ if (target === undefined) {
1022
+ throw usageError(`Knowledge not found: ${parsed.update}.`);
1023
+ }
1024
+ if (target.status !== "active") {
1025
+ throw usageError(`Knowledge is not active: ${target.id}.`);
1026
+ }
1027
+ knowledgeId = target.id;
1028
+ next = updateProjectKnowledge(next, knowledgeId, {
1029
+ title: proposal.title,
1030
+ body: proposal.body
1031
+ }, now);
1032
+ next = withKnowledgeProvenance(next, knowledgeId, {
1033
+ ...provenanceFromProposal(proposal),
1034
+ proposalId: proposal.id,
1035
+ promotedBy: decidedBy,
1036
+ promotedAt: now.toISOString()
1037
+ }, now);
1038
+ }
1039
+ else {
1040
+ const plan = planKnowledgeAcceptance(next, proposal);
1041
+ if (plan.kind === "duplicate") {
1042
+ knowledgeId = plan.knowledgeId;
1043
+ duplicate = true;
1044
+ }
1045
+ else {
1046
+ if (plan.kind === "create" && plan.supersedesKnowledgeId !== undefined) {
1047
+ next = retireProjectKnowledge(next, plan.supersedesKnowledgeId, now);
1048
+ }
1049
+ knowledgeId = nextKnowledgeId(next);
1050
+ next = addProjectKnowledge(next, knowledgeId, proposal.title, proposal.body, now, {
1051
+ ...provenanceFromProposal(proposal),
1052
+ proposalId: proposal.id,
1053
+ promotedBy: decidedBy,
1054
+ promotedAt: now.toISOString()
1055
+ });
1056
+ }
1057
+ }
1058
+ next = decideKnowledgeProposal(next, proposal.id, {
1059
+ by: decidedBy,
1060
+ knowledgeId
1061
+ }, now);
1062
+ tx.saveProject(next);
1063
+ return { projectId: next.id, proposalId: proposal.id, knowledgeId, duplicate };
1064
+ });
1065
+ return {
1066
+ output: `${accepted.duplicate
1067
+ ? `Knowledge proposal ${accepted.proposalId} was already promoted as ${accepted.knowledgeId}`
1068
+ : `Accepted knowledge proposal ${accepted.proposalId} as ${accepted.knowledgeId}`} in ${accepted.projectId}\n`,
1069
+ projectId: accepted.projectId,
1070
+ proposalId: accepted.proposalId,
1071
+ knowledgeId: accepted.knowledgeId
1072
+ };
1073
+ }
1074
+ function rejectKnowledgeProposal(args, store, options) {
1075
+ const usage = "Project knowledge reject usage: yui project knowledge reject <project> <proposal>"
1076
+ + " --reason <text>.";
1077
+ const parsed = parseSingleOption(args, "--reason", usage);
1078
+ if (parsed.positionals.length !== 2)
1079
+ throw usageError(usage);
1080
+ assertKnowledgeOperator(options, "reject");
1081
+ const actor = projectActor(options.environment);
1082
+ const decidedBy = actor === "operator" ? "operator" : "user";
1083
+ const rejected = store.transaction((tx) => {
1084
+ const project = requireProject(tx, parsed.positionals[0]);
1085
+ const proposal = findKnowledgeProposal(project, parsed.positionals[1]);
1086
+ if (proposal === null) {
1087
+ throw usageError(`Knowledge proposal not found: ${parsed.positionals[1]}.`);
1088
+ }
1089
+ if (proposal.status !== "pending") {
1090
+ throw usageError(`Knowledge proposal is already ${proposal.status}: ${proposal.id}.`);
1091
+ }
1092
+ const next = decideKnowledgeProposal(project, proposal.id, {
1093
+ by: decidedBy,
1094
+ reason: parsed.value
1095
+ }, (options.now ?? (() => new Date()))());
1096
+ tx.saveProject(next);
1097
+ return { projectId: next.id, proposalId: proposal.id };
1098
+ });
1099
+ return {
1100
+ output: `Rejected knowledge proposal ${rejected.proposalId} in ${rejected.projectId}\n`,
1101
+ projectId: rejected.projectId,
1102
+ proposalId: rejected.proposalId
1103
+ };
1104
+ }
1105
+ function provenanceFromProposal(proposal) {
1106
+ return {
1107
+ taskId: proposal.source.taskId,
1108
+ ...(proposal.source.decisionId === undefined ? {} : { decisionId: proposal.source.decisionId }),
1109
+ ...(proposal.source.milestoneId === undefined ? {} : { milestoneId: proposal.source.milestoneId }),
1110
+ ...(proposal.source.commitSha === undefined ? {} : { commitSha: proposal.source.commitSha }),
1111
+ ...(proposal.evidenceDigest === undefined ? {} : { evidenceDigest: proposal.evidenceDigest }),
1112
+ fingerprint: proposal.fingerprint
1113
+ };
1114
+ }
1115
+ /** Replace a Knowledge entry's provenance (used by accept --update). */
1116
+ function withKnowledgeProvenance(project, knowledgeId, provenance, now) {
1117
+ const timestamp = now.toISOString();
1118
+ const knowledge = project.knowledge.map((entry) => (entry.id === knowledgeId
1119
+ ? { ...entry, provenance, updatedAt: timestamp }
1120
+ : entry));
1121
+ return validateProject({ ...project, knowledge, updatedAt: timestamp });
1122
+ }
1123
+ function parseAcceptArguments(args, usage) {
1124
+ const positionals = [];
1125
+ let update;
1126
+ for (let index = 0; index < args.length; index += 1) {
1127
+ const value = args[index];
1128
+ if (value === "--update") {
1129
+ if (update !== undefined)
1130
+ throw usageError(`--update may only be provided once. ${usage}`);
1131
+ const next = args[index + 1];
1132
+ if (next === undefined || next.startsWith("--"))
1133
+ throw usageError(`--update is required. ${usage}`);
1134
+ update = requireText(next, "--update");
1135
+ index += 1;
1136
+ }
1137
+ else if (value.startsWith("--")) {
1138
+ throw usageError(`Unknown option: ${value}. ${usage}`);
1139
+ }
1140
+ else {
1141
+ positionals.push(value);
1142
+ }
1143
+ }
1144
+ if (positionals.length !== 2)
1145
+ throw usageError(usage);
1146
+ return { project: positionals[0], proposal: positionals[1], ...(update === undefined ? {} : { update }) };
1147
+ }
646
1148
  function parseAddArguments(args, usage) {
647
1149
  const positionals = [];
648
1150
  const aliases = [];