@zq-silk/yui 0.6.13 → 0.6.15

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 +172 -62
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +102 -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 +518 -55
  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 +71 -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 +191 -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 +16 -2
  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 +253 -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 +198 -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 +7 -1
  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 +289 -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 +17 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -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"),
@@ -592,27 +610,16 @@ function projectKnowledge(args, store, options) {
592
610
  };
593
611
  }
594
612
  if (command === "update") {
595
- const usage = "Project knowledge update usage: yui project knowledge update <project> <knowledge> [--title <text>] [--body <text>].";
596
- const parsed = parseKnowledgeUpdateArguments(rest, usage);
597
- const updated = store.transaction((tx) => {
598
- const project = requireProject(tx, parsed.project);
599
- const next = updateProjectKnowledge(project, parsed.id, {
600
- ...(parsed.title === undefined ? {} : { title: parsed.title }),
601
- ...(parsed.body === undefined ? {} : { body: parsed.body })
602
- }, (options.now ?? (() => new Date()))());
603
- tx.saveProject(next);
604
- return next;
605
- });
606
- return {
607
- output: `Updated project knowledge ${parsed.id} in ${updated.id}\n`,
608
- projectId: updated.id,
609
- knowledgeId: parsed.id
610
- };
613
+ throw usageError("Direct Project Knowledge update is not supported because it would erase version history. "
614
+ + "Propose the replacement with `yui project knowledge propose <project> ... --task <task>` "
615
+ + "and let the Operator apply it with `yui project knowledge accept <project> <proposal> "
616
+ + "--update <knowledge>`; use --supersedes when replacing Knowledge without proposal history.");
611
617
  }
612
618
  if (command === "retire") {
613
619
  if (rest.length !== 2) {
614
620
  throw usageError("Project knowledge retire usage: yui project knowledge retire <project> <knowledge>.");
615
621
  }
622
+ assertKnowledgeOperator(options, "retire");
616
623
  const updated = store.transaction((tx) => {
617
624
  const project = requireProject(tx, rest[0]);
618
625
  const next = retireProjectKnowledge(project, rest[1], (options.now ?? (() => new Date()))());
@@ -625,6 +632,24 @@ function projectKnowledge(args, store, options) {
625
632
  knowledgeId: rest[1]
626
633
  };
627
634
  }
635
+ if (command === "propose") {
636
+ return proposeKnowledge(rest, store, options);
637
+ }
638
+ if (command === "proposals") {
639
+ const [sub, ...subRest] = rest;
640
+ if (sub === "list")
641
+ return listKnowledgeProposals(subRest, store);
642
+ if (sub === "show")
643
+ return showKnowledgeProposal(subRest, store);
644
+ throw usageError("Project knowledge proposals usage: yui project knowledge proposals list <project>"
645
+ + " [--status pending|accepted|rejected] [--all] | yui project knowledge proposals show <project> <proposal>.");
646
+ }
647
+ if (command === "accept") {
648
+ return acceptKnowledgeProposal(rest, store, options);
649
+ }
650
+ if (command === "reject") {
651
+ return rejectKnowledgeProposal(rest, store, options);
652
+ }
628
653
  throw usageError(command === undefined
629
654
  ? "Project knowledge command is required."
630
655
  : `Unknown command: project knowledge ${command}`);
@@ -643,6 +668,481 @@ function nextKnowledgeId(project) {
643
668
  return id;
644
669
  }
645
670
  }
671
+ function nextProposalId(project) {
672
+ const used = new Set(project.knowledgeProposals.map(({ id }) => id));
673
+ for (let index = 1;; index += 1) {
674
+ const id = `proposal-${index}`;
675
+ if (!used.has(id))
676
+ return id;
677
+ }
678
+ }
679
+ /**
680
+ * Project Knowledge is an Operator authority. A managed Task Session (Leader,
681
+ * Reviewer, Worker) must use `propose` and let an Operator accept; it can
682
+ * never write the authoritative list directly.
683
+ */
684
+ function assertKnowledgeOperator(options, action) {
685
+ const actor = projectActor(options.environment);
686
+ if (actor === "agent") {
687
+ throw usageError(`Project Knowledge is an Operator authority; a managed Task Session cannot ${action} it.`
688
+ + " Propose a candidate with `yui project knowledge propose` instead.");
689
+ }
690
+ }
691
+ const PROPOSE_OPTIONS = [
692
+ "--title", "--body", "--task", "--decision", "--milestone",
693
+ "--commit", "--scope", "--expires-when", "--supersedes"
694
+ ];
695
+ function parseKnowledgeProposalOptions(args, usage) {
696
+ const values = {};
697
+ const positionals = [];
698
+ for (let index = 0; index < args.length; index += 1) {
699
+ const value = args[index];
700
+ if (PROPOSE_OPTIONS.includes(value)) {
701
+ if (values[value] !== undefined) {
702
+ throw usageError(`${value} may only be provided once. ${usage}`);
703
+ }
704
+ const next = args[index + 1];
705
+ if (next === undefined || next.startsWith("--")) {
706
+ throw usageError(`${value} is required. ${usage}`);
707
+ }
708
+ values[value] = requireText(next, value);
709
+ index += 1;
710
+ }
711
+ else if (value.startsWith("--")) {
712
+ throw usageError(`Unknown option: ${value}. ${usage}`);
713
+ }
714
+ else {
715
+ positionals.push(value);
716
+ }
717
+ }
718
+ if (positionals.length !== 1)
719
+ throw usageError(usage);
720
+ if (values["--title"] === undefined || values["--body"] === undefined || values["--task"] === undefined) {
721
+ throw usageError(usage);
722
+ }
723
+ return {
724
+ project: positionals[0],
725
+ title: values["--title"],
726
+ body: values["--body"],
727
+ task: values["--task"],
728
+ ...(values["--decision"] === undefined ? {} : { decision: values["--decision"] }),
729
+ ...(values["--milestone"] === undefined ? {} : { milestone: values["--milestone"] }),
730
+ ...(values["--commit"] === undefined ? {} : { commit: values["--commit"] }),
731
+ ...(values["--scope"] === undefined ? {} : { scope: values["--scope"] }),
732
+ ...(values["--expires-when"] === undefined ? {} : { expiresWhen: values["--expires-when"] }),
733
+ ...(values["--supersedes"] === undefined ? {} : { supersedes: values["--supersedes"] })
734
+ };
735
+ }
736
+ /**
737
+ * Leader-proposed promotion candidate. The proposal is workflow state, not
738
+ * Knowledge: it never appears in `project knowledge list/show` until an
739
+ * Operator accepts it. A managed Task Session may propose only for its own
740
+ * Task. The same candidate (source identity + title + body) is deduplicated to
741
+ * the existing pending proposal instead of creating a second copy.
742
+ */
743
+ function proposeKnowledge(args, store, options) {
744
+ const usage = "Project knowledge propose usage: yui project knowledge propose <project>"
745
+ + " --title <text> --body <text> --task <task> [--decision <id>] [--milestone <id>]"
746
+ + " [--commit <sha>] [--scope <text>] [--expires-when <text>] [--supersedes <knowledge-id>].";
747
+ const parsed = parseKnowledgeProposalOptions(args, usage);
748
+ const actor = projectActor(options.environment);
749
+ if (actor === "agent" && options.environment?.YUI_TASK_ID !== parsed.task) {
750
+ throw usageError("A managed Task Session may propose Knowledge only for its own Task.");
751
+ }
752
+ if (actor === "agent"
753
+ && options.environment?.YUI_ROLE !== "leader"
754
+ && options.environment?.YUI_ROLE !== "reviewer") {
755
+ throw usageError("Only a Leader or Reviewer Session may propose Knowledge promotion.");
756
+ }
757
+ const proposedBy = actor === "agent"
758
+ ? (options.environment?.YUI_ROLE === "reviewer" ? "reviewer" : "leader")
759
+ : actor;
760
+ const now = (options.now ?? (() => new Date()))();
761
+ const result = store.transaction((tx) => {
762
+ const project = requireProject(tx, parsed.project);
763
+ const source = requireProposalEvidence(tx, parsed);
764
+ const fingerprint = knowledgeProposalFingerprint({
765
+ projectId: project.id,
766
+ source,
767
+ title: parsed.title,
768
+ body: parsed.body
769
+ });
770
+ const pending = findKnowledgeProposalByFingerprint(project, fingerprint, "pending");
771
+ if (pending !== null) {
772
+ return {
773
+ projectId: project.id,
774
+ proposalId: pending.id,
775
+ deduped: true,
776
+ alreadyAccepted: false
777
+ };
778
+ }
779
+ const accepted = findKnowledgeProposalByFingerprint(project, fingerprint, "accepted");
780
+ if (accepted !== null) {
781
+ return {
782
+ projectId: project.id,
783
+ proposalId: accepted.id,
784
+ knowledgeId: accepted.knowledgeId,
785
+ deduped: true,
786
+ alreadyAccepted: true
787
+ };
788
+ }
789
+ const proposal = {
790
+ schemaVersion: 1,
791
+ id: nextProposalId(project),
792
+ projectId: project.id,
793
+ title: parsed.title,
794
+ body: parsed.body,
795
+ status: "pending",
796
+ source,
797
+ ...(parsed.scope === undefined ? {} : { scope: parsed.scope }),
798
+ ...(parsed.expiresWhen === undefined ? {} : { expiresWhen: parsed.expiresWhen }),
799
+ ...(parsed.supersedes === undefined ? {} : { supersedesKnowledgeId: parsed.supersedes }),
800
+ fingerprint,
801
+ ...(source.evidenceDigest === undefined ? {} : { evidenceDigest: source.evidenceDigest }),
802
+ proposedBy,
803
+ proposedAt: now.toISOString(),
804
+ updatedAt: now.toISOString()
805
+ };
806
+ tx.saveProject(addKnowledgeProposal(project, proposal));
807
+ return {
808
+ projectId: project.id,
809
+ proposalId: proposal.id,
810
+ deduped: false,
811
+ alreadyAccepted: false
812
+ };
813
+ });
814
+ const lines = [
815
+ result.deduped
816
+ ? `Knowledge proposal ${result.proposalId} already exists for ${result.projectId}`
817
+ : `Proposed knowledge ${result.proposalId} for ${result.projectId}`,
818
+ result.alreadyAccepted
819
+ ? `Already accepted as knowledge ${result.knowledgeId ?? "?"}`
820
+ : "Awaiting Operator decision: yui project knowledge proposals list "
821
+ + `${result.projectId} / yui project knowledge accept ${result.projectId} ${result.proposalId}`
822
+ ];
823
+ return {
824
+ output: `${lines.join("\n")}\n`,
825
+ projectId: result.projectId,
826
+ proposalId: result.proposalId,
827
+ ...(result.knowledgeId === undefined ? {} : { knowledgeId: result.knowledgeId })
828
+ };
829
+ }
830
+ /**
831
+ * Verify the cited Task/Decision/Milestone exist and capture the evidence
832
+ * digest at proposal time, so a later Agent can verify the source record.
833
+ */
834
+ function requireProposalEvidence(store, parsed) {
835
+ if (store.getTask === undefined) {
836
+ throw usageError("Knowledge promotion requires Task evidence, which this store does not provide.");
837
+ }
838
+ const task = store.getTask(parsed.task);
839
+ if (task === null)
840
+ throw usageError(`Task not found: ${parsed.task}.`);
841
+ const source = {
842
+ taskId: task.id,
843
+ ...(parsed.decision === undefined ? {} : { decisionId: parsed.decision }),
844
+ ...(parsed.milestone === undefined ? {} : { milestoneId: parsed.milestone }),
845
+ ...(parsed.commit === undefined ? {} : { commitSha: parsed.commit })
846
+ };
847
+ let evidence;
848
+ if (parsed.decision !== undefined) {
849
+ if (store.getDecision === undefined) {
850
+ throw usageError("Knowledge promotion requires Decision evidence, which this store does not provide.");
851
+ }
852
+ const decision = store.getDecision(task.id, parsed.decision);
853
+ if (decision === null) {
854
+ throw usageError(`Decision not found: ${parsed.decision} in Task ${task.id}.`);
855
+ }
856
+ evidence = `${decision.title}\n${decision.rationale}`;
857
+ }
858
+ else if (parsed.milestone !== undefined) {
859
+ if (store.getMilestone === undefined) {
860
+ throw usageError("Knowledge promotion requires Milestone evidence, which this store does not provide.");
861
+ }
862
+ const milestone = store.getMilestone(task.id, parsed.milestone);
863
+ if (milestone === null) {
864
+ throw usageError(`Milestone not found: ${parsed.milestone} in Task ${task.id}.`);
865
+ }
866
+ evidence = `${milestone.title}\n${milestone.summary}`;
867
+ }
868
+ else {
869
+ evidence = task.completionSummary ?? task.title;
870
+ }
871
+ return { ...source, evidenceDigest: knowledgeEvidenceDigest(evidence) };
872
+ }
873
+ function listKnowledgeProposals(args, store) {
874
+ const usage = "Project knowledge proposals list usage: yui project knowledge proposals list <project>"
875
+ + " [--status pending|accepted|rejected] [--all].";
876
+ const positionals = [];
877
+ let status = "pending";
878
+ let sawAll = false;
879
+ let sawStatus = false;
880
+ for (let index = 0; index < args.length; index += 1) {
881
+ const value = args[index];
882
+ if (value === "--all") {
883
+ if (sawStatus)
884
+ throw usageError(`--all and --status are mutually exclusive. ${usage}`);
885
+ sawAll = true;
886
+ status = undefined;
887
+ }
888
+ else if (value === "--status") {
889
+ if (sawAll)
890
+ throw usageError(`--all and --status are mutually exclusive. ${usage}`);
891
+ sawStatus = true;
892
+ const next = args[index + 1];
893
+ if (next === undefined || next.startsWith("--"))
894
+ throw usageError(usage);
895
+ if (next !== "pending" && next !== "accepted" && next !== "rejected")
896
+ throw usageError(usage);
897
+ status = next;
898
+ index += 1;
899
+ }
900
+ else if (value.startsWith("--")) {
901
+ throw usageError(`Unknown option: ${value}. ${usage}`);
902
+ }
903
+ else {
904
+ positionals.push(value);
905
+ }
906
+ }
907
+ if (positionals.length !== 1)
908
+ throw usageError(usage);
909
+ const project = requireProject(store, positionals[0]);
910
+ const entries = project.knowledgeProposals
911
+ .filter((entry) => status === undefined || entry.status === status)
912
+ .slice()
913
+ .sort((a, b) => a.id.localeCompare(b.id));
914
+ if (entries.length === 0) {
915
+ return {
916
+ output: "No knowledge proposals found.\n",
917
+ projectId: project.id,
918
+ proposals: []
919
+ };
920
+ }
921
+ return {
922
+ output: `${renderTable(`Knowledge proposals: ${project.name}`, [
923
+ { header: "Proposal", minWidth: 9, maxWidth: 24 },
924
+ { header: "Title", minWidth: 8, maxWidth: 48 },
925
+ { header: "Status", minWidth: 7, maxWidth: 10 },
926
+ { header: "Source", minWidth: 8, maxWidth: 32 },
927
+ { header: "Knowledge", minWidth: 9, maxWidth: 24 }
928
+ ], entries.map((entry) => [
929
+ entry.id,
930
+ entry.title,
931
+ entry.status,
932
+ `${entry.source.taskId}`
933
+ + `${entry.source.decisionId === undefined ? "" : `/${entry.source.decisionId}`}`
934
+ + `${entry.source.milestoneId === undefined ? "" : `/${entry.source.milestoneId}`}`,
935
+ entry.knowledgeId ?? "-"
936
+ ]), defaultTableWidth())}\n`,
937
+ projectId: project.id,
938
+ proposals: entries
939
+ };
940
+ }
941
+ function showKnowledgeProposal(args, store) {
942
+ const usage = "Project knowledge proposals show usage: yui project knowledge proposals show <project> <proposal>.";
943
+ if (args.length !== 2)
944
+ throw usageError(usage);
945
+ const project = requireProject(store, args[0]);
946
+ const proposal = findKnowledgeProposal(project, args[1]);
947
+ if (proposal === null) {
948
+ throw usageError(`Knowledge proposal not found: ${args[1]}.`);
949
+ }
950
+ const lines = [
951
+ `Proposal: ${proposal.id}`,
952
+ `Project: ${project.id}`,
953
+ `Title: ${proposal.title}`,
954
+ `Status: ${proposal.status}`,
955
+ `Source: ${proposal.source.taskId}`
956
+ + `${proposal.source.decisionId === undefined ? "" : `/${proposal.source.decisionId}`}`
957
+ + `${proposal.source.milestoneId === undefined ? "" : `/${proposal.source.milestoneId}`}`
958
+ + `${proposal.source.commitSha === undefined ? "" : `@${proposal.source.commitSha}`}`,
959
+ `Proposed by: ${proposal.proposedBy} at ${proposal.proposedAt}`,
960
+ ...(proposal.scope === undefined ? [] : [`Scope: ${proposal.scope}`]),
961
+ ...(proposal.expiresWhen === undefined ? [] : [`Expires when: ${proposal.expiresWhen}`]),
962
+ ...(proposal.supersedesKnowledgeId === undefined
963
+ ? []
964
+ : [`Supersedes: ${proposal.supersedesKnowledgeId}`]),
965
+ ...(proposal.evidenceDigest === undefined ? [] : [`Evidence digest: ${proposal.evidenceDigest}`]),
966
+ ...(proposal.decidedBy === undefined ? [] : [`Decided by: ${proposal.decidedBy} at ${proposal.decidedAt ?? "?"}`]),
967
+ ...(proposal.decisionReason === undefined ? [] : [`Decision reason: ${proposal.decisionReason}`]),
968
+ ...(proposal.knowledgeId === undefined ? [] : [`Knowledge: ${proposal.knowledgeId}`]),
969
+ "",
970
+ proposal.body
971
+ ];
972
+ return {
973
+ output: `${lines.join("\n")}\n`,
974
+ projectId: project.id,
975
+ proposalId: proposal.id,
976
+ proposals: [proposal]
977
+ };
978
+ }
979
+ /**
980
+ * Operator acceptance. The pure {@link planKnowledgeAcceptance} policy decides
981
+ * create/update/duplicate/conflict; a conflict fails closed so a candidate can
982
+ * never silently overwrite an existing conclusion. The accepted Knowledge
983
+ * entry carries the source provenance and fingerprint, and the proposal record
984
+ * retains the decision for history.
985
+ */
986
+ function acceptKnowledgeProposal(args, store, options) {
987
+ const usage = "Project knowledge accept usage: yui project knowledge accept <project> <proposal>"
988
+ + " [--update <knowledge-id>].";
989
+ const parsed = parseAcceptArguments(args, usage);
990
+ assertKnowledgeOperator(options, "accept");
991
+ const actor = projectActor(options.environment);
992
+ const decidedBy = actor === "operator" ? "operator" : "user";
993
+ const now = (options.now ?? (() => new Date()))();
994
+ const accepted = store.transaction((tx) => {
995
+ const project = requireProject(tx, parsed.project);
996
+ const proposal = findKnowledgeProposal(project, parsed.proposal);
997
+ if (proposal === null) {
998
+ throw usageError(`Knowledge proposal not found: ${parsed.proposal}.`);
999
+ }
1000
+ if (proposal.status !== "pending") {
1001
+ throw usageError(`Knowledge proposal is already ${proposal.status}: ${proposal.id}.`);
1002
+ }
1003
+ let next = project;
1004
+ let knowledgeId;
1005
+ let duplicate = false;
1006
+ if (parsed.update !== undefined) {
1007
+ const target = next.knowledge.find((entry) => entry.id === parsed.update);
1008
+ if (target === undefined) {
1009
+ throw usageError(`Knowledge not found: ${parsed.update}.`);
1010
+ }
1011
+ if (target.status !== "active") {
1012
+ throw usageError(`Knowledge is not active: ${target.id}.`);
1013
+ }
1014
+ const previousProposal = target.provenance?.proposalId === undefined
1015
+ ? null
1016
+ : findKnowledgeProposal(next, target.provenance.proposalId);
1017
+ if (previousProposal === null
1018
+ || previousProposal.status !== "accepted"
1019
+ || previousProposal.knowledgeId !== target.id
1020
+ || previousProposal.title !== target.title
1021
+ || previousProposal.body !== target.body) {
1022
+ throw usageError(`Knowledge ${target.id} has no complete proposal-backed version history. `
1023
+ + "Create the replacement proposal with --supersedes instead of --update.");
1024
+ }
1025
+ knowledgeId = target.id;
1026
+ next = updateProjectKnowledge(next, knowledgeId, {
1027
+ title: proposal.title,
1028
+ body: proposal.body
1029
+ }, now);
1030
+ next = withKnowledgeProvenance(next, knowledgeId, {
1031
+ ...provenanceFromProposal(proposal),
1032
+ proposalId: proposal.id,
1033
+ promotedBy: decidedBy,
1034
+ promotedAt: now.toISOString()
1035
+ }, now);
1036
+ }
1037
+ else {
1038
+ const plan = planKnowledgeAcceptance(next, proposal);
1039
+ if (plan.kind === "duplicate") {
1040
+ knowledgeId = plan.knowledgeId;
1041
+ duplicate = true;
1042
+ }
1043
+ else {
1044
+ if (plan.kind === "create" && plan.supersedesKnowledgeId !== undefined) {
1045
+ next = retireProjectKnowledge(next, plan.supersedesKnowledgeId, now);
1046
+ }
1047
+ knowledgeId = nextKnowledgeId(next);
1048
+ next = addProjectKnowledge(next, knowledgeId, proposal.title, proposal.body, now, {
1049
+ ...provenanceFromProposal(proposal),
1050
+ proposalId: proposal.id,
1051
+ promotedBy: decidedBy,
1052
+ promotedAt: now.toISOString()
1053
+ });
1054
+ }
1055
+ }
1056
+ next = decideKnowledgeProposal(next, proposal.id, {
1057
+ by: decidedBy,
1058
+ knowledgeId
1059
+ }, now);
1060
+ tx.saveProject(next);
1061
+ return { projectId: next.id, proposalId: proposal.id, knowledgeId, duplicate };
1062
+ });
1063
+ return {
1064
+ output: `${accepted.duplicate
1065
+ ? `Knowledge proposal ${accepted.proposalId} was already promoted as ${accepted.knowledgeId}`
1066
+ : `Accepted knowledge proposal ${accepted.proposalId} as ${accepted.knowledgeId}`} in ${accepted.projectId}\n`,
1067
+ projectId: accepted.projectId,
1068
+ proposalId: accepted.proposalId,
1069
+ knowledgeId: accepted.knowledgeId
1070
+ };
1071
+ }
1072
+ function rejectKnowledgeProposal(args, store, options) {
1073
+ const usage = "Project knowledge reject usage: yui project knowledge reject <project> <proposal>"
1074
+ + " --reason <text>.";
1075
+ const parsed = parseSingleOption(args, "--reason", usage);
1076
+ if (parsed.positionals.length !== 2)
1077
+ throw usageError(usage);
1078
+ assertKnowledgeOperator(options, "reject");
1079
+ const actor = projectActor(options.environment);
1080
+ const decidedBy = actor === "operator" ? "operator" : "user";
1081
+ const rejected = store.transaction((tx) => {
1082
+ const project = requireProject(tx, parsed.positionals[0]);
1083
+ const proposal = findKnowledgeProposal(project, parsed.positionals[1]);
1084
+ if (proposal === null) {
1085
+ throw usageError(`Knowledge proposal not found: ${parsed.positionals[1]}.`);
1086
+ }
1087
+ if (proposal.status !== "pending") {
1088
+ throw usageError(`Knowledge proposal is already ${proposal.status}: ${proposal.id}.`);
1089
+ }
1090
+ const next = decideKnowledgeProposal(project, proposal.id, {
1091
+ by: decidedBy,
1092
+ reason: parsed.value
1093
+ }, (options.now ?? (() => new Date()))());
1094
+ tx.saveProject(next);
1095
+ return { projectId: next.id, proposalId: proposal.id };
1096
+ });
1097
+ return {
1098
+ output: `Rejected knowledge proposal ${rejected.proposalId} in ${rejected.projectId}\n`,
1099
+ projectId: rejected.projectId,
1100
+ proposalId: rejected.proposalId
1101
+ };
1102
+ }
1103
+ function provenanceFromProposal(proposal) {
1104
+ return {
1105
+ taskId: proposal.source.taskId,
1106
+ ...(proposal.source.decisionId === undefined ? {} : { decisionId: proposal.source.decisionId }),
1107
+ ...(proposal.source.milestoneId === undefined ? {} : { milestoneId: proposal.source.milestoneId }),
1108
+ ...(proposal.source.commitSha === undefined ? {} : { commitSha: proposal.source.commitSha }),
1109
+ ...(proposal.evidenceDigest === undefined ? {} : { evidenceDigest: proposal.evidenceDigest }),
1110
+ fingerprint: proposal.fingerprint
1111
+ };
1112
+ }
1113
+ /** Replace a Knowledge entry's provenance (used by accept --update). */
1114
+ function withKnowledgeProvenance(project, knowledgeId, provenance, now) {
1115
+ const timestamp = now.toISOString();
1116
+ const knowledge = project.knowledge.map((entry) => (entry.id === knowledgeId
1117
+ ? { ...entry, provenance, updatedAt: timestamp }
1118
+ : entry));
1119
+ return validateProject({ ...project, knowledge, updatedAt: timestamp });
1120
+ }
1121
+ function parseAcceptArguments(args, usage) {
1122
+ const positionals = [];
1123
+ let update;
1124
+ for (let index = 0; index < args.length; index += 1) {
1125
+ const value = args[index];
1126
+ if (value === "--update") {
1127
+ if (update !== undefined)
1128
+ throw usageError(`--update may only be provided once. ${usage}`);
1129
+ const next = args[index + 1];
1130
+ if (next === undefined || next.startsWith("--"))
1131
+ throw usageError(`--update is required. ${usage}`);
1132
+ update = requireText(next, "--update");
1133
+ index += 1;
1134
+ }
1135
+ else if (value.startsWith("--")) {
1136
+ throw usageError(`Unknown option: ${value}. ${usage}`);
1137
+ }
1138
+ else {
1139
+ positionals.push(value);
1140
+ }
1141
+ }
1142
+ if (positionals.length !== 2)
1143
+ throw usageError(usage);
1144
+ return { project: positionals[0], proposal: positionals[1], ...(update === undefined ? {} : { update }) };
1145
+ }
646
1146
  function parseAddArguments(args, usage) {
647
1147
  const positionals = [];
648
1148
  const aliases = [];
@@ -753,43 +1253,6 @@ function parseProjectUpdateArguments(args, usage) {
753
1253
  ...(development === undefined ? {} : { development })
754
1254
  };
755
1255
  }
756
- function parseKnowledgeUpdateArguments(args, usage) {
757
- const positionals = [];
758
- let title;
759
- let body;
760
- for (let index = 0; index < args.length; index += 1) {
761
- const value = args[index];
762
- if (value === "--title" || value === "--body") {
763
- const next = args[index + 1];
764
- if (next === undefined || next.startsWith("--"))
765
- throw usageError(`${value} is required. ${usage}`);
766
- if (value === "--title") {
767
- if (title !== undefined)
768
- throw usageError(`--title may only be provided once. ${usage}`);
769
- title = requireText(next, value);
770
- }
771
- else {
772
- if (body !== undefined)
773
- throw usageError(`--body may only be provided once. ${usage}`);
774
- body = requireText(next, value);
775
- }
776
- index += 1;
777
- continue;
778
- }
779
- if (value.startsWith("--"))
780
- throw usageError(`Unknown option: ${value}. ${usage}`);
781
- positionals.push(value);
782
- }
783
- if (positionals.length !== 2 || (title === undefined && body === undefined)) {
784
- throw usageError(usage);
785
- }
786
- return {
787
- project: positionals[0],
788
- id: positionals[1],
789
- ...(title === undefined ? {} : { title }),
790
- ...(body === undefined ? {} : { body })
791
- };
792
- }
793
1256
  function parseCloneArguments(args, usage) {
794
1257
  const positionals = [];
795
1258
  const aliases = [];
@@ -368,7 +368,6 @@ const CONTROLLER_OPERATIONAL_ENV = [
368
368
  "TZ",
369
369
  "LANG",
370
370
  "TERM",
371
- "YUI_TMUX_BIN"
372
371
  ];
373
372
  function spawnDetachedController(home, releaseDir, extraEnv) {
374
373
  // Fail closed before launching: a release that drifted from its manifest
@@ -29,6 +29,23 @@ export function taskActor(environment, taskId) {
29
29
  }
30
30
  return "user";
31
31
  }
32
+ export function projectActor(environment) {
33
+ const env = environment ?? {};
34
+ if (env.YUI_SESSION_SCOPE === "task")
35
+ return "agent";
36
+ if (env.YUI_SESSION_SCOPE === "global") {
37
+ if (env.YUI_ROLE === "operator")
38
+ return "operator";
39
+ throw usageError("A managed global Session may manage Project Knowledge only as Operator.");
40
+ }
41
+ if (env.YUI_ROLE !== undefined
42
+ || env.YUI_AGENT_ID !== undefined
43
+ || env.YUI_RUN_ID !== undefined
44
+ || env.YUI_NATIVE_SESSION_ID !== undefined) {
45
+ throw usageError("Managed Agent identity is incomplete; refusing to infer user authority.");
46
+ }
47
+ return "user";
48
+ }
32
49
  /**
33
50
  * rr8: Resolve the caller identity for a `job.start`/`job.cancel` request from
34
51
  * the managed Session environment. The Controller binds the declared job owner