@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.
- package/README.md +34 -5
- package/dist/cli/commandCatalog.js +172 -62
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +102 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +518 -55
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +71 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +191 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +16 -2
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +253 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +198 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +7 -1
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +289 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +17 -3
- 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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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 = [];
|
|
@@ -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
|