@consensus-tools/consensus-tools 0.1.2 → 0.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # consensus.tools
2
2
 
3
- ![consensus-tools](./assets/consensus-tools)
3
+ ![consensus-tools](https://cdn.jsdelivr.net/npm/@consensus-tools/consensus-tools@latest/assets/consensus-tools.png)
4
4
 
5
5
  **High-confidence decisions for agentic systems.**
6
6
  Local-first. Incentive-aligned. Verifiable.
@@ -386,7 +386,7 @@ Example (see full schema in `openclaw.plugin.json`):
386
386
  "maxParticipants": 3,
387
387
  "minParticipants": 1,
388
388
  "expiresSeconds": 86400,
389
- "consensusPolicy": { "type": "SINGLE_WINNER", "trustedArbiterAgentId": "" },
389
+ "consensusPolicy": { "type": "FIRST_SUBMISSION_WINS", "trustedArbiterAgentId": "", "tieBreak": "earliest" },
390
390
  "slashingPolicy": { "enabled": false, "slashPercent": 0, "slashFlat": 0 }
391
391
  },
392
392
  "ledger": { "faucetEnabled": false, "initialCreditsPerAgent": 0, "balancesMode": "initial", "balances": {} }
Binary file
@@ -61,8 +61,8 @@
61
61
  "properties": {
62
62
  "type": {
63
63
  "type": "string",
64
- "enum": ["SINGLE_WINNER", "MAJORITY_VOTE", "WEIGHTED_REPUTATION", "TRUSTED_ARBITER"],
65
- "default": "SINGLE_WINNER"
64
+ "enum": ["FIRST_SUBMISSION_WINS", "APPROVAL_VOTE", "MAJORITY_VOTE", "WEIGHTED_REPUTATION", "TRUSTED_ARBITER"],
65
+ "default": "FIRST_SUBMISSION_WINS"
66
66
  },
67
67
  "trustedArbiterAgentId": { "type": "string", "default": "" }
68
68
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consensus-tools/consensus-tools",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "bin",
19
19
  "index.ts",
20
20
  "src",
21
+ "assets",
21
22
  "openclaw.plugin.json",
22
23
  "README.md"
23
24
  ],
package/src/cli.ts CHANGED
@@ -237,16 +237,23 @@ function registerConsensusSubcommands(
237
237
  .description('Cast a vote')
238
238
  .option('--submission <id>', 'Submission id to vote for')
239
239
  .option('--choice <key>', 'Choice key to vote for')
240
+ .option('--yes', 'Approval vote: yes (+1)')
241
+ .option('--no', 'Approval vote: no (-1)')
240
242
  .option('--weight <n>', 'Vote weight', parseFloat)
243
+ .option('--stake <n>', 'Stake amount locked for this vote (staked settlement)', parseFloat)
241
244
  .option('--json', 'JSON output')
242
245
  .action(async (jobId: string, opts: any) => {
246
+ const hasYesNo = Boolean(opts.yes || opts.no);
247
+ const score = hasYesNo ? (opts.no ? -1 : 1) : (opts.weight ?? 1);
248
+
243
249
  const vote = await backend.vote(agentId, jobId, {
244
250
  submissionId: opts.submission,
245
251
  choiceKey: opts.choice,
246
252
  targetType: opts.submission ? 'SUBMISSION' : opts.choice ? 'CHOICE' : undefined,
247
253
  targetId: opts.submission ?? opts.choice,
248
- weight: opts.weight ?? 1,
249
- score: opts.weight ?? 1
254
+ weight: opts.weight ?? (hasYesNo ? 1 : undefined),
255
+ score,
256
+ stakeAmount: opts.stake
250
257
  });
251
258
  output(vote, opts.json);
252
259
  });
@@ -582,6 +589,16 @@ function commonSh(): string {
582
589
  ' echo "$CONSENSUS_ROOT"',
583
590
  '}',
584
591
  '',
592
+ 'local_state_file() {',
593
+ ' # Engine-parity local state file (JsonStorage)',
594
+ ' if [[ -n "${CONSENSUS_STATE_FILE:-}" ]]; then',
595
+ ' echo "${CONSENSUS_STATE_FILE}"',
596
+ ' else',
597
+ ' echo "$(local_root)/state.json"',
598
+ ' fi',
599
+ '}',
600
+ '',
601
+ '',
585
602
  'ensure_local_board() {',
586
603
  ' local root; root="$(local_root)"',
587
604
  ' mkdir -p "$root/jobs"',
@@ -683,39 +700,35 @@ function jobsPostSh(): string {
683
700
  '',
684
701
  'POLICY="${CONSENSUS_DEFAULT_POLICY:-HIGHEST_CONFIDENCE_SINGLE}"',
685
702
  'REWARD="${CONSENSUS_DEFAULT_REWARD:-8}"',
686
- 'STAKE="${CONSENSUS_DEFAULT_STAKE:-4}"',
687
- 'LEASE_SECONDS="${CONSENSUS_DEFAULT_LEASE_SECONDS:-180}"',
703
+ 'STAKE_REQUIRED="${CONSENSUS_DEFAULT_STAKE:-4}"',
704
+ 'EXPIRES_SECONDS="${CONSENSUS_DEFAULT_EXPIRES_SECONDS:-86400}"',
688
705
  'MODE="$(mode)"',
689
706
  '',
690
707
  'if [[ "$MODE" == "local" ]]; then',
691
708
  ' ensure_local_board',
692
- ' local id; id="$(rand_id "job")"',
693
- '',
694
- ' local title_json desc_json input_json',
695
- ' title_json="$(json_escape "$TITLE")"',
696
- ' desc_json="$(json_escape "${DESC:-}")"',
697
- ' input_json="$(json_escape "${INPUT:-}")"',
698
- '',
699
- ' local job_json',
700
- ' job_json="$(cat <<JSON',
701
- '{',
702
- ' "id": "$id",',
703
- ' "title": $title_json,',
704
- ' "desc": $desc_json,',
705
- ' "input": $input_json,',
706
- ' "mode": "SUBMISSION",',
707
- ' "policyKey": "$POLICY",',
708
- ' "rewardAmount": $REWARD,',
709
- ' "stakeAmount": $STAKE,',
710
- ' "leaseSeconds": $LEASE_SECONDS,',
711
- ' "status": "OPEN",',
712
- ' "createdAt": "$(now_iso)"',
713
- '}',
714
- 'JSON',
715
- ')"',
716
- ' write_json_file "$(job_file "$id")" "$job_json"',
717
- ' ensure_job_dir "$id"',
718
- ' echo "$job_json"',
709
+ ' state_file="$(local_state_file)"',
710
+ ' node --import tsx --input-type=module - "$state_file" "$TITLE" "$DESC" "$INPUT" "$POLICY" "$REWARD" "$STAKE_REQUIRED" "$EXPIRES_SECONDS" <<\'NODE\'',
711
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
712
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
713
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
714
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
715
+ 'const [stateFile,title,desc,input,policy,reward,stake,expires]=process.argv.slice(2);',
716
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
717
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
718
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
719
+ "const job = await engine.postJob('cli@local', {",
720
+ ' title,',
721
+ ' desc,',
722
+ ' inputRef: input,',
723
+ ' policyKey: policy,',
724
+ ' rewardAmount: Number(reward),',
725
+ ' reward: Number(reward),',
726
+ ' stakeRequired: Number(stake),',
727
+ ' stakeAmount: Number(stake),',
728
+ ' expiresSeconds: Number(expires)',
729
+ '});',
730
+ 'console.log(JSON.stringify(job, null, 2));',
731
+ 'NODE',
719
732
  ' exit 0',
720
733
  'fi',
721
734
  '',
@@ -729,14 +742,13 @@ function jobsPostSh(): string {
729
742
  ' "mode": "SUBMISSION",',
730
743
  ' "policyKey": "$POLICY",',
731
744
  ' "rewardAmount": $REWARD,',
732
- ' "stakeAmount": $STAKE,',
733
- ' "leaseSeconds": $LEASE_SECONDS',
745
+ ' "stakeAmount": $STAKE_REQUIRED,',
746
+ ' "leaseSeconds": ${CONSENSUS_DEFAULT_LEASE_SECONDS:-180}',
734
747
  '}',
735
748
  'JSON',
736
749
  ')"',
737
750
  'curl_json "POST" "$base/jobs" "$payload"',
738
751
  'echo',
739
- ''
740
752
  ].join('\n');
741
753
  }
742
754
 
@@ -756,14 +768,26 @@ function jobsGetSh(): string {
756
768
  '',
757
769
  'if [[ "$MODE" == "local" ]]; then',
758
770
  ' ensure_local_board',
759
- ' read_json_file "$(job_file "$JOB_ID")"',
771
+ ' state_file="$(local_state_file)"',
772
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
773
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
774
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
775
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
776
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
777
+ 'const [stateFile, jobId] = process.argv.slice(2);',
778
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
779
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
780
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
781
+ 'const job = await engine.getJob(jobId);',
782
+ "if (!job) { console.error('Job not found'); process.exit(1); }",
783
+ 'console.log(JSON.stringify(job, null, 2));',
784
+ 'NODE',
760
785
  ' exit 0',
761
786
  'fi',
762
787
  '',
763
788
  'base="$(remote_base)"',
764
789
  'curl -sS "$base/jobs/$JOB_ID" -H "$(remote_auth_header)"',
765
790
  'echo',
766
- ''
767
791
  ].join('\n');
768
792
  }
769
793
 
@@ -777,13 +801,23 @@ function jobsListSh(): string {
777
801
  '',
778
802
  'if [[ "$MODE" == "local" ]]; then',
779
803
  ' ensure_local_board',
780
- ' root="$(local_root)"',
781
- ' ls -1 "$root/jobs"/*.json 2>/dev/null | sed "s#.*/##" | sed "s#\\.json$##" || true',
804
+ ' state_file="$(local_state_file)"',
805
+ ' node --import tsx --input-type=module - "$state_file" <<\'NODE\'',
806
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
807
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
808
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
809
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
810
+ 'const [stateFile]=process.argv.slice(2);',
811
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
812
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
813
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
814
+ 'const jobs = await engine.listJobs({});',
815
+ 'for (const j of jobs) console.log(j.id);',
816
+ 'NODE',
782
817
  ' exit 0',
783
818
  'fi',
784
819
  '',
785
820
  'base="$(remote_base)"',
786
- '# Optional: pass query string as $1, e.g. "status=OPEN&mode=SUBMISSION"',
787
821
  'QS="${1:-}"',
788
822
  'url="$base/jobs"',
789
823
  'if [[ -n "$QS" ]]; then',
@@ -791,7 +825,6 @@ function jobsListSh(): string {
791
825
  'fi',
792
826
  'curl -sS "$url" -H "$(remote_auth_header)"',
793
827
  'echo',
794
- ''
795
828
  ].join('\n');
796
829
  }
797
830
 
@@ -804,10 +837,10 @@ function submissionsCreateSh(): string {
804
837
  'JOB_ID="${1:-}"',
805
838
  'ARTIFACT_JSON="${2:-}"',
806
839
  'SUMMARY="${3:-}"',
840
+ 'CONFIDENCE="${4:-0.5}"',
807
841
  '',
808
842
  'if [[ -z "$JOB_ID" || -z "$ARTIFACT_JSON" ]]; then',
809
- ' echo "Usage: submissions_create.sh <jobId> <artifact_json> [summary]" >&2',
810
- ' echo "Example: submissions_create.sh job_... {\\\"toxic\\\":false,\\\"confidence\\\":0.98,\\\"brief_reason\\\":\\\"...\\\"}" >&2',
843
+ ' echo "Usage: submissions_create.sh <jobId> <artifact_json> [summary] [confidence]" >&2',
811
844
  ' exit 2',
812
845
  'fi',
813
846
  '',
@@ -815,23 +848,20 @@ function submissionsCreateSh(): string {
815
848
  '',
816
849
  'if [[ "$MODE" == "local" ]]; then',
817
850
  ' ensure_local_board',
818
- ' ensure_job_dir "$JOB_ID"',
819
- '',
820
- ' sid="$(rand_id "sub")"',
821
- ' summary_json="$(json_escape "${SUMMARY:-}")"',
822
- ' sub_json="$(cat <<JSON',
823
- '{',
824
- ' "id": "$sid",',
825
- ' "jobId": "$JOB_ID",',
826
- ' "artifact": $ARTIFACT_JSON,',
827
- ' "summary": $summary_json,',
828
- ' "createdAt": "$(now_iso)",',
829
- ' "status": "VALID"',
830
- '}',
831
- 'JSON',
832
- ')"',
833
- ' write_json_file "$(job_dir "$JOB_ID")/submissions/${sid}.json" "$sub_json"',
834
- ' echo "$sub_json"',
851
+ ' state_file="$(local_state_file)"',
852
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" "$ARTIFACT_JSON" "${SUMMARY:-}" "$CONFIDENCE" <<\'NODE\'',
853
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
854
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
855
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
856
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
857
+ 'const [stateFile, jobId, artifactJson, summary, conf] = process.argv.slice(2);',
858
+ 'const artifacts = JSON.parse(artifactJson);',
859
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
860
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
861
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
862
+ "const sub = await engine.submitJob('cli@local', jobId, { summary: summary || '', artifacts, confidence: Number(conf) });",
863
+ 'console.log(JSON.stringify(sub, null, 2));',
864
+ 'NODE',
835
865
  ' exit 0',
836
866
  'fi',
837
867
  '',
@@ -845,7 +875,6 @@ function submissionsCreateSh(): string {
845
875
  ')"',
846
876
  'curl_json "POST" "$base/jobs/$JOB_ID/submissions" "$payload"',
847
877
  'echo',
848
- ''
849
878
  ].join('\n');
850
879
  }
851
880
 
@@ -865,15 +894,25 @@ function submissionsListSh(): string {
865
894
  '',
866
895
  'if [[ "$MODE" == "local" ]]; then',
867
896
  ' ensure_local_board',
868
- ' ensure_job_dir "$JOB_ID"',
869
- ' ls -1 "$(job_dir "$JOB_ID")/submissions"/*.json 2>/dev/null | xargs -I{} cat "{}" || true',
897
+ ' state_file="$(local_state_file)"',
898
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
899
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
900
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
901
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
902
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
903
+ 'const [stateFile, jobId] = process.argv.slice(2);',
904
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
905
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
906
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
907
+ 'const list = await engine.listSubmissions(jobId);',
908
+ 'for (const s of list) console.log(JSON.stringify(s, null, 2));',
909
+ 'NODE',
870
910
  ' exit 0',
871
911
  'fi',
872
912
  '',
873
913
  'base="$(remote_base)"',
874
914
  'curl -sS "$base/jobs/$JOB_ID/submissions" -H "$(remote_auth_header)"',
875
915
  'echo',
876
- ''
877
916
  ].join('\n');
878
917
  }
879
918
 
@@ -884,14 +923,27 @@ function votesCastSh(): string {
884
923
  'source "$(dirname "$0")/common.sh"',
885
924
  '',
886
925
  'JOB_ID="${1:-}"',
887
- 'TARGET_TYPE="${2:-}" # SUBMISSION or CHOICE',
888
- 'TARGET_ID="${3:-}" # submission id or choice key',
889
- 'WEIGHT="${4:-1}"',
926
+ 'shift || true',
927
+ 'TARGET_TYPE=""',
928
+ 'TARGET_ID=""',
929
+ 'WEIGHT="1"',
930
+ '',
931
+ 'if [[ "${1:-}" == "SUBMISSION" || "${1:-}" == "CHOICE" ]]; then',
932
+ ' TARGET_TYPE="$1"; TARGET_ID="${2:-}"; WEIGHT="${3:-1}"',
933
+ 'else',
934
+ ' while [[ $# -gt 0 ]]; do',
935
+ ' case "$1" in',
936
+ ' --submission) TARGET_TYPE="SUBMISSION"; TARGET_ID="${2:-}"; shift 2 ;;',
937
+ ' --choice) TARGET_TYPE="CHOICE"; TARGET_ID="${2:-}"; shift 2 ;;',
938
+ ' --weight) WEIGHT="${2:-1}"; shift 2 ;;',
939
+ ' *) echo "Unknown arg: $1" >&2; exit 2 ;;',
940
+ ' esac',
941
+ ' done',
942
+ 'fi',
890
943
  '',
891
944
  'if [[ -z "$JOB_ID" || -z "$TARGET_TYPE" || -z "$TARGET_ID" ]]; then',
892
- ' echo "Usage: votes_cast.sh <jobId> <targetType:SUBMISSION|CHOICE> <targetId> [weight]" >&2',
893
- ' echo "Example: votes_cast.sh job_... SUBMISSION sub_... 1" >&2',
894
- ' echo "Example: votes_cast.sh job_... CHOICE TOXIC_FALSE 1" >&2',
945
+ ' echo "Usage: votes_cast.sh <jobId> SUBMISSION <submissionId> [weight]" >&2',
946
+ ' echo " or: votes_cast.sh <jobId> --submission <id> [--weight <n>]" >&2',
895
947
  ' exit 2',
896
948
  'fi',
897
949
  '',
@@ -899,22 +951,19 @@ function votesCastSh(): string {
899
951
  '',
900
952
  'if [[ "$MODE" == "local" ]]; then',
901
953
  ' ensure_local_board',
902
- ' ensure_job_dir "$JOB_ID"',
903
- '',
904
- ' vid="$(rand_id "vote")"',
905
- ' vote_json="$(cat <<JSON',
906
- '{',
907
- ' "id": "$vid",',
908
- ' "jobId": "$JOB_ID",',
909
- ' "targetType": "$TARGET_TYPE",',
910
- ' "targetId": "$TARGET_ID",',
911
- ' "weight": $WEIGHT,',
912
- ' "createdAt": "$(now_iso)"',
913
- '}',
914
- 'JSON',
915
- ')"',
916
- ' write_json_file "$(job_dir "$JOB_ID")/votes/${vid}.json" "$vote_json"',
917
- ' echo "$vote_json"',
954
+ ' state_file="$(local_state_file)"',
955
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" "$TARGET_TYPE" "$TARGET_ID" "$WEIGHT" <<\'NODE\'',
956
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
957
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
958
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
959
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
960
+ 'const [stateFile, jobId, targetType, targetId, weight] = process.argv.slice(2);',
961
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
962
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
963
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
964
+ "const vote = await engine.vote('cli@local', jobId, { targetType, targetId, submissionId: targetType==='SUBMISSION'?targetId:undefined, choiceKey: targetType==='CHOICE'?targetId:undefined, weight: Number(weight), score: Number(weight) });",
965
+ 'console.log(JSON.stringify(vote, null, 2));',
966
+ 'NODE',
918
967
  ' exit 0',
919
968
  'fi',
920
969
  '',
@@ -929,7 +978,6 @@ function votesCastSh(): string {
929
978
  ')"',
930
979
  'curl_json "POST" "$base/jobs/$JOB_ID/votes" "$payload"',
931
980
  'echo',
932
- ''
933
981
  ].join('\n');
934
982
  }
935
983
 
@@ -949,15 +997,25 @@ function votesListSh(): string {
949
997
  '',
950
998
  'if [[ "$MODE" == "local" ]]; then',
951
999
  ' ensure_local_board',
952
- ' ensure_job_dir "$JOB_ID"',
953
- ' ls -1 "$(job_dir "$JOB_ID")/votes"/*.json 2>/dev/null | xargs -I{} cat "{}" || true',
1000
+ ' state_file="$(local_state_file)"',
1001
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
1002
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
1003
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
1004
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
1005
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
1006
+ 'const [stateFile, jobId] = process.argv.slice(2);',
1007
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
1008
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
1009
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
1010
+ 'const list = await engine.listVotes(jobId);',
1011
+ 'for (const v of list) console.log(JSON.stringify(v, null, 2));',
1012
+ 'NODE',
954
1013
  ' exit 0',
955
1014
  'fi',
956
1015
  '',
957
1016
  'base="$(remote_base)"',
958
1017
  'curl -sS "$base/jobs/$JOB_ID/votes" -H "$(remote_auth_header)"',
959
1018
  'echo',
960
- ''
961
1019
  ].join('\n');
962
1020
  }
963
1021
 
@@ -968,8 +1026,20 @@ function resolveSh(): string {
968
1026
  'source "$(dirname "$0")/common.sh"',
969
1027
  '',
970
1028
  'JOB_ID="${1:-}"',
1029
+ 'shift || true',
1030
+ 'MANUAL_WINNER=""',
1031
+ 'MANUAL_SUB=""',
1032
+ '',
1033
+ 'while [[ $# -gt 0 ]]; do',
1034
+ ' case "$1" in',
1035
+ ' --winner) MANUAL_WINNER="${2:-}"; shift 2 ;;',
1036
+ ' --submission) MANUAL_SUB="${2:-}"; shift 2 ;;',
1037
+ ' *) echo "Unknown arg: $1" >&2; exit 2 ;;',
1038
+ ' esac',
1039
+ 'done',
1040
+ '',
971
1041
  'if [[ -z "$JOB_ID" ]]; then',
972
- ' echo "Usage: resolve.sh <jobId>" >&2',
1042
+ ' echo "Usage: resolve.sh <jobId> [--winner <agentId>] [--submission <submissionId>]" >&2',
973
1043
  ' exit 2',
974
1044
  'fi',
975
1045
  '',
@@ -977,56 +1047,29 @@ function resolveSh(): string {
977
1047
  '',
978
1048
  'if [[ "$MODE" == "local" ]]; then',
979
1049
  ' ensure_local_board',
980
- ' ensure_job_dir "$JOB_ID"',
981
- '',
982
- ' # Local resolution policy: HIGHEST_CONFIDENCE_SINGLE for SUBMISSION jobs.',
983
- ' # We pick the submission with max artifact.confidence if present.',
984
- ' # If missing, we fall back to the most recent submission.',
985
- '',
986
- ' dir="$(job_dir "$JOB_ID")/submissions"',
987
- ' if ! ls "$dir"/*.json >/dev/null 2>&1; then',
988
- ' echo "No submissions found for $JOB_ID" >&2',
989
- ' exit 1',
990
- ' fi',
991
- '',
992
- ' python3 - <<\'PY\' "$JOB_ID" "$dir" | tee "$(job_dir "$JOB_ID")/result.json"',
993
- 'import json,glob,sys,os',
994
- 'job_id=sys.argv[1]; d=sys.argv[2]',
995
- 'subs=[]',
996
- 'for p in glob.glob(os.path.join(d,"*.json")):',
997
- ' with open(p,"r") as f:',
998
- ' s=json.load(f)',
999
- ' conf=None',
1000
- ' try:',
1001
- ' conf=float(s.get("artifact",{}).get("confidence"))',
1002
- ' except Exception:',
1003
- ' conf=None',
1004
- ' subs.append((conf,s.get("createdAt",""),s,p))',
1005
- '# sort: confidence desc (None last), then createdAt desc',
1006
- 'def key(t):',
1007
- ' conf,created,_,_ = t',
1008
- ' return (conf is not None, conf if conf is not None else -1.0, created)',
1009
- 'subs_sorted=sorted(subs, key=key, reverse=True)',
1010
- 'conf,created,s,p=subs_sorted[0]',
1011
- 'result={',
1012
- ' "jobId": job_id,',
1013
- ' "mode": "SUBMISSION",',
1014
- ' "selectedSubmissionId": s.get("id"),',
1015
- ' "selectedSubmissionPath": p,',
1016
- ' "resolvedAt": __import__("datetime").datetime.utcnow().isoformat()+"Z",',
1017
- ' "artifact": s.get("artifact"),',
1018
- ' "summary": s.get("summary","")',
1019
- '}',
1020
- 'print(json.dumps(result, indent=2))',
1021
- 'PY',
1022
- '',
1050
+ ' state_file="$(local_state_file)"',
1051
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" "$MANUAL_WINNER" "$MANUAL_SUB" <<\'NODE\'',
1052
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
1053
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
1054
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
1055
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
1056
+ 'const [stateFile, jobId, winner, subId] = process.argv.slice(2);',
1057
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
1058
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
1059
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
1060
+ 'const input:any = {};',
1061
+ 'if (winner) input.manualWinners = [winner];',
1062
+ 'if (subId) input.manualSubmissionId = subId;',
1063
+ "const actor = winner || 'cli@local';",
1064
+ 'const res = await engine.resolveJob(actor, jobId, input);',
1065
+ 'console.log(JSON.stringify(res, null, 2));',
1066
+ 'NODE',
1023
1067
  ' exit 0',
1024
1068
  'fi',
1025
1069
  '',
1026
1070
  'base="$(remote_base)"',
1027
1071
  'curl -sS -X POST "$base/jobs/$JOB_ID/resolve" -H "$(remote_auth_header)"',
1028
1072
  'echo',
1029
- ''
1030
1073
  ].join('\n');
1031
1074
  }
1032
1075
 
@@ -1046,14 +1089,24 @@ function resultGetSh(): string {
1046
1089
  '',
1047
1090
  'if [[ "$MODE" == "local" ]]; then',
1048
1091
  ' ensure_local_board',
1049
- ' path="$(job_dir "$JOB_ID")/result.json"',
1050
- ' read_json_file "$path"',
1092
+ ' state_file="$(local_state_file)"',
1093
+ ' node --import tsx --input-type=module - "$state_file" "$JOB_ID" <<\'NODE\'',
1094
+ "import { JsonStorage } from '@consensus-tools/consensus-tools/src/storage/JsonStorage.ts';",
1095
+ "import { LedgerEngine } from '@consensus-tools/consensus-tools/src/ledger/ledger.ts';",
1096
+ "import { JobEngine } from '@consensus-tools/consensus-tools/src/jobs/engine.ts';",
1097
+ "import { defaultConfig } from '@consensus-tools/consensus-tools/src/config.ts';",
1098
+ 'const [stateFile, jobId] = process.argv.slice(2);',
1099
+ 'const storage=new JsonStorage(stateFile); await storage.init();',
1100
+ 'const ledger=new LedgerEngine(storage, defaultConfig);',
1101
+ 'const engine=new JobEngine(storage, ledger, defaultConfig);',
1102
+ 'const status = await engine.getStatus(jobId);',
1103
+ 'console.log(JSON.stringify(status.resolution ?? null, null, 2));',
1104
+ 'NODE',
1051
1105
  ' exit 0',
1052
1106
  'fi',
1053
1107
  '',
1054
1108
  'base="$(remote_base)"',
1055
1109
  'curl -sS "$base/jobs/$JOB_ID/result" -H "$(remote_auth_header)"',
1056
1110
  'echo',
1057
- ''
1058
1111
  ].join('\n');
1059
1112
  }
package/src/config.ts CHANGED
@@ -50,8 +50,9 @@ export const configSchema = {
50
50
  type: {
51
51
  type: 'string',
52
52
  enum: [
53
- 'SINGLE_WINNER',
53
+ 'FIRST_SUBMISSION_WINS',
54
54
  'HIGHEST_CONFIDENCE_SINGLE',
55
+ 'APPROVAL_VOTE',
55
56
  'OWNER_PICK',
56
57
  'TOP_K_SPLIT',
57
58
  'MAJORITY_VOTE',
@@ -59,7 +60,7 @@ export const configSchema = {
59
60
  'WEIGHTED_REPUTATION',
60
61
  'TRUSTED_ARBITER'
61
62
  ],
62
- default: 'SINGLE_WINNER'
63
+ default: 'FIRST_SUBMISSION_WINS'
63
64
  },
64
65
  trustedArbiterAgentId: { type: 'string', default: '' },
65
66
  minConfidence: { type: 'number', default: 0, minimum: 0, maximum: 1 },
@@ -99,8 +100,9 @@ export const configSchema = {
99
100
  type: {
100
101
  type: 'string',
101
102
  enum: [
102
- 'SINGLE_WINNER',
103
+ 'FIRST_SUBMISSION_WINS',
103
104
  'HIGHEST_CONFIDENCE_SINGLE',
105
+ 'APPROVAL_VOTE',
104
106
  'OWNER_PICK',
105
107
  'TOP_K_SPLIT',
106
108
  'MAJORITY_VOTE',
@@ -190,12 +192,13 @@ export const defaultConfig: ConsensusToolsConfig = {
190
192
  maxParticipants: 3,
191
193
  minParticipants: 1,
192
194
  expiresSeconds: 86400,
193
- consensusPolicy: { type: 'SINGLE_WINNER', trustedArbiterAgentId: '' },
195
+ consensusPolicy: { type: 'FIRST_SUBMISSION_WINS', trustedArbiterAgentId: '', tieBreak: 'earliest' },
194
196
  slashingPolicy: { enabled: false, slashPercent: 0, slashFlat: 0 }
195
197
  },
196
198
  consensusPolicies: {
197
- SINGLE_WINNER: { type: 'SINGLE_WINNER' },
199
+ FIRST_SUBMISSION_WINS: { type: 'FIRST_SUBMISSION_WINS' },
198
200
  HIGHEST_CONFIDENCE_SINGLE: { type: 'HIGHEST_CONFIDENCE_SINGLE', minConfidence: 0 },
201
+ APPROVAL_VOTE: { type: 'APPROVAL_VOTE', quorum: 1, minScore: 1, minMargin: 0, tieBreak: 'earliest', approvalVote: { weightMode: 'equal', settlement: 'immediate' } },
199
202
  OWNER_PICK: { type: 'OWNER_PICK' },
200
203
  TOP_K_SPLIT: { type: 'TOP_K_SPLIT', topK: 2, ordering: 'confidence' },
201
204
  MAJORITY_VOTE: { type: 'MAJORITY_VOTE' },
package/src/initWizard.ts CHANGED
@@ -12,9 +12,10 @@ export type InitWizardResult = {
12
12
 
13
13
  const POLICY_CHOICES = [
14
14
  'HIGHEST_CONFIDENCE_SINGLE',
15
+ 'APPROVAL_VOTE',
15
16
  'TOP_K_SPLIT',
16
17
  'OWNER_PICK',
17
- 'SINGLE_WINNER',
18
+ 'FIRST_SUBMISSION_WINS',
18
19
  'MAJORITY_VOTE',
19
20
  'WEIGHTED_VOTE_SIMPLE',
20
21
  'WEIGHTED_REPUTATION',
@@ -56,7 +56,7 @@ export function resolveConsensus(input: ConsensusInput): ConsensusResult {
56
56
  return { winners: [], winningSubmissionIds: [], consensusTrace: { policy, reason: 'no_submissions' }, finalArtifact: null };
57
57
  }
58
58
 
59
- if (policy === 'SINGLE_WINNER') {
59
+ if (policy === 'FIRST_SUBMISSION_WINS') {
60
60
  const sorted = [...input.submissions].sort((a, b) => Date.parse(a.submittedAt) - Date.parse(b.submittedAt));
61
61
  const winner = sorted[0];
62
62
  return {
@@ -67,6 +67,121 @@ export function resolveConsensus(input: ConsensusInput): ConsensusResult {
67
67
  };
68
68
  }
69
69
 
70
+ if (policy === 'APPROVAL_VOTE') {
71
+ const quorum = input.job.consensusPolicy.quorum;
72
+ const minScore = input.job.consensusPolicy.minScore ?? 1;
73
+ const minMargin = input.job.consensusPolicy.minMargin ?? 0;
74
+ const tieBreak = input.job.consensusPolicy.tieBreak ?? 'earliest';
75
+
76
+ const weightMode = input.job.consensusPolicy.approvalVote?.weightMode ?? 'equal';
77
+ const settlement = input.job.consensusPolicy.approvalVote?.settlement ?? 'immediate';
78
+
79
+ // Oracle settlement can always be manually finalized by the arbiter, even with no votes.
80
+ if (settlement === 'oracle' && input.manualWinnerAgentIds && input.manualWinnerAgentIds.length) {
81
+ return {
82
+ winners: input.manualWinnerAgentIds,
83
+ winningSubmissionIds: input.manualSubmissionId ? [input.manualSubmissionId] : [],
84
+ consensusTrace: { policy, settlement, mode: 'manual' },
85
+ finalArtifact: findArtifact(input, input.manualSubmissionId)
86
+ };
87
+ }
88
+
89
+ const scores: Record<string, number> = {};
90
+ const voteCounts: Record<string, number> = {};
91
+
92
+ // Only consider votes that target submissions.
93
+ const votes = input.votes.filter((v) => v.submissionId || (v.targetType === 'SUBMISSION' && v.targetId));
94
+ if (quorum && votes.length < quorum) {
95
+ return {
96
+ winners: [],
97
+ winningSubmissionIds: [],
98
+ consensusTrace: { policy, settlement, reason: 'quorum_not_met', quorum, votes: votes.length },
99
+ finalArtifact: null
100
+ };
101
+ }
102
+
103
+ for (const vote of votes) {
104
+ const sid = vote.submissionId ?? (vote.targetType === 'SUBMISSION' ? vote.targetId : undefined);
105
+ if (!sid) continue;
106
+
107
+ let weight = 1;
108
+ if (weightMode === 'explicit') weight = vote.weight ?? 1;
109
+ if (weightMode === 'reputation') weight = input.reputation(vote.agentId);
110
+
111
+ // score should be +1 (YES) or -1 (NO); clamp to [-1,1] to avoid weirdness.
112
+ const s = Math.max(-1, Math.min(1, vote.score ?? 0));
113
+ scores[sid] = (scores[sid] || 0) + s * weight;
114
+ voteCounts[sid] = (voteCounts[sid] || 0) + 1;
115
+ }
116
+
117
+ // rank submissions by score desc
118
+ const ranked = input.submissions
119
+ .map((sub) => ({ sub, score: scores[sub.id] || 0, votes: voteCounts[sub.id] || 0 }))
120
+ .sort((a, b) => {
121
+ if (b.score === a.score) {
122
+ if (tieBreak === 'confidence') return b.sub.confidence - a.sub.confidence;
123
+ // default earliest
124
+ return Date.parse(a.sub.submittedAt) - Date.parse(b.sub.submittedAt);
125
+ }
126
+ return b.score - a.score;
127
+ });
128
+
129
+ const best = ranked[0];
130
+ const second = ranked[1];
131
+ const margin = second ? best.score - second.score : best.score;
132
+
133
+ if (!best || best.votes === 0) {
134
+ return {
135
+ winners: [],
136
+ winningSubmissionIds: [],
137
+ consensusTrace: { policy, settlement, reason: 'no_votes', scores, voteCounts },
138
+ finalArtifact: null
139
+ };
140
+ }
141
+
142
+ if (best.score < minScore || margin < minMargin) {
143
+ return {
144
+ winners: [],
145
+ winningSubmissionIds: [],
146
+ consensusTrace: { policy, settlement, reason: 'threshold_not_met', minScore, minMargin, best: best.score, margin, scores, voteCounts },
147
+ finalArtifact: null
148
+ };
149
+ }
150
+
151
+ if (settlement === 'oracle' || tieBreak === 'arbiter') {
152
+ // Oracle / arbiter settlement: allow manual finalization, otherwise provide a recommendation.
153
+ if (input.manualWinnerAgentIds && input.manualWinnerAgentIds.length) {
154
+ return {
155
+ winners: input.manualWinnerAgentIds,
156
+ winningSubmissionIds: input.manualSubmissionId ? [input.manualSubmissionId] : [],
157
+ consensusTrace: {
158
+ policy,
159
+ settlement,
160
+ mode: 'manual',
161
+ recommendedSubmissionId: best.sub.id,
162
+ recommendedAgentId: best.sub.agentId,
163
+ scores,
164
+ voteCounts
165
+ },
166
+ finalArtifact: findArtifact(input, input.manualSubmissionId)
167
+ };
168
+ }
169
+ return {
170
+ winners: [],
171
+ winningSubmissionIds: [],
172
+ consensusTrace: { policy, settlement, mode: 'awaiting_oracle', recommendedSubmissionId: best.sub.id, recommendedAgentId: best.sub.agentId, scores, voteCounts },
173
+ finalArtifact: null
174
+ };
175
+ }
176
+
177
+ return {
178
+ winners: [best.sub.agentId],
179
+ winningSubmissionIds: [best.sub.id],
180
+ consensusTrace: { policy, settlement, scores, voteCounts, minScore, minMargin, tieBreak },
181
+ finalArtifact: best.sub.artifacts
182
+ };
183
+ }
184
+
70
185
  if (policy === 'HIGHEST_CONFIDENCE_SINGLE') {
71
186
  const minConfidence = input.job.consensusPolicy.minConfidence ?? 0;
72
187
  const sorted = [...input.submissions]
@@ -297,11 +297,13 @@ export class JobEngine {
297
297
  async vote(agentId: string, jobId: string, input: VoteInput): Promise<Vote> {
298
298
  const now = nowIso();
299
299
  return (await this.storage.update((state) => {
300
+ // optional: stake on votes (APPROVAL_VOTE staked settlement)
301
+ const stakeAmount = input.stakeAmount ? Math.max(0, Number(input.stakeAmount)) : 0;
300
302
  const job = state.jobs.find((j) => j.id === jobId);
301
303
  if (!job) throw new Error(`Job not found: ${jobId}`);
302
304
  if (
303
305
  job.mode === 'SUBMISSION' &&
304
- (job.consensusPolicy.type === 'SINGLE_WINNER' ||
306
+ (job.consensusPolicy.type === 'FIRST_SUBMISSION_WINS' ||
305
307
  job.consensusPolicy.type === 'HIGHEST_CONFIDENCE_SINGLE' ||
306
308
  job.consensusPolicy.type === 'OWNER_PICK' ||
307
309
  job.consensusPolicy.type === 'TOP_K_SPLIT' ||
@@ -309,6 +311,22 @@ export class JobEngine {
309
311
  ) {
310
312
  throw new Error('Voting not enabled for this job');
311
313
  }
314
+
315
+ // optional vote stake (only meaningful for APPROVAL_VOTE settlement=staked)
316
+ if (stakeAmount > 0) {
317
+ const currentBalance = getBalance(state.ledger, agentId);
318
+ const nextBalance = currentBalance - Math.abs(stakeAmount);
319
+ ensureNonNegative(nextBalance, `${agentId} vote stake for ${jobId}`);
320
+ state.ledger.push({
321
+ id: newId('ledger'),
322
+ at: now,
323
+ type: 'STAKE',
324
+ agentId,
325
+ amount: -Math.abs(stakeAmount),
326
+ jobId,
327
+ reason: 'vote'
328
+ });
329
+ }
312
330
  const targetType = input.targetType ?? (input.submissionId ? 'SUBMISSION' : input.choiceKey ? 'CHOICE' : undefined);
313
331
  const targetId = input.targetId ?? input.submissionId;
314
332
  if (targetType === 'SUBMISSION') {
@@ -330,7 +348,7 @@ export class JobEngine {
330
348
  agentId,
331
349
  score,
332
350
  weight: input.weight ?? score,
333
- stakeAmount: input.stakeAmount,
351
+ stakeAmount: stakeAmount || undefined,
334
352
  rationale: input.rationale,
335
353
  createdAt: now
336
354
  };
@@ -359,10 +377,28 @@ export class JobEngine {
359
377
  if (arbiter && arbiter !== agentId) {
360
378
  throw new Error('Only the trusted arbiter can resolve this job');
361
379
  }
380
+ if (!input.manualWinners || input.manualWinners.length === 0) {
381
+ throw new Error('Trusted arbiter must provide manual winners to resolve');
382
+ }
362
383
  }
363
384
 
364
- if (job.consensusPolicy.type === 'OWNER_PICK' && job.createdByAgentId !== agentId) {
365
- throw new Error('Only the job creator can resolve this job');
385
+ if (job.consensusPolicy.type === 'OWNER_PICK') {
386
+ if (job.createdByAgentId !== agentId) {
387
+ throw new Error('Only the job creator can resolve this job');
388
+ }
389
+ if (!input.manualWinners || input.manualWinners.length === 0) {
390
+ throw new Error('Owner must provide manual winners to resolve');
391
+ }
392
+ }
393
+
394
+ if (job.consensusPolicy.type === 'APPROVAL_VOTE' && job.consensusPolicy.approvalVote?.settlement === 'oracle') {
395
+ const arbiter = job.consensusPolicy.trustedArbiterAgentId;
396
+ if (arbiter && arbiter !== agentId) {
397
+ throw new Error('Only the trusted arbiter can resolve this job');
398
+ }
399
+ if (!input.manualWinners || input.manualWinners.length === 0) {
400
+ throw new Error('Oracle settlement requires manual winners to resolve');
401
+ }
366
402
  }
367
403
 
368
404
  const submissions = state.submissions.filter((s) => s.jobId === jobId);
@@ -398,6 +434,41 @@ export class JobEngine {
398
434
  }
399
435
  }
400
436
 
437
+ // Vote-stake settlement for APPROVAL_VOTE (best-effort, local-first)
438
+ if (job.consensusPolicy.type === 'APPROVAL_VOTE' && job.consensusPolicy.approvalVote?.settlement === 'staked') {
439
+ const winnerSubmissionId = consensus.winningSubmissionIds?.[0];
440
+ const voteSlashPercent = Math.max(0, Math.min(1, job.consensusPolicy.approvalVote?.voteSlashPercent ?? 0));
441
+ for (const v of votes) {
442
+ const st = v.stakeAmount ?? 0;
443
+ if (!st || st <= 0) continue;
444
+
445
+ // Return stake by default
446
+ state.ledger.push({
447
+ id: newId('ledger'),
448
+ at: now,
449
+ type: 'UNSTAKE',
450
+ agentId: v.agentId,
451
+ amount: st,
452
+ jobId,
453
+ reason: 'vote'
454
+ });
455
+
456
+ // Slash if vote is "wrong" relative to winner.
457
+ // Wrong = YES on non-winner OR NO on winner.
458
+ const votedSubmissionId = v.submissionId ?? (v.targetType === 'SUBMISSION' ? v.targetId : undefined);
459
+ const isYes = (v.score ?? 0) > 0;
460
+ const isNo = (v.score ?? 0) < 0;
461
+ const wrong =
462
+ (winnerSubmissionId && votedSubmissionId && votedSubmissionId !== winnerSubmissionId && isYes) ||
463
+ (winnerSubmissionId && votedSubmissionId && votedSubmissionId === winnerSubmissionId && isNo);
464
+
465
+ if (wrong && voteSlashPercent > 0) {
466
+ const slashAmount = Math.min(st, st * voteSlashPercent);
467
+ slashes.push({ agentId: v.agentId, amount: slashAmount, reason: 'vote_wrong' });
468
+ }
469
+ }
470
+ }
471
+
401
472
  const submissionAgents = new Set(submissions.map((s) => s.agentId));
402
473
  for (const bid of bids) {
403
474
  const stakeAmount = bid.stakeAmount;
package/src/standalone.ts CHANGED
@@ -163,7 +163,7 @@ function helpText() {
163
163
  ' jobs list [--tag <tag>] [--status <status>] [--mine] [--json]',
164
164
  ' submissions create <jobId> --artifact <json> [--summary <text>] [--confidence <n>] [--json]',
165
165
  ' submissions list <jobId> [--json]',
166
- ' votes cast <jobId> [--submission <id> | --choice <key>] [--weight <n>] [--json]',
166
+ ' votes cast <jobId> [--submission <id> | --choice <key>] [--yes|--no] [--weight <n>] [--stake <n>] [--json]',
167
167
  ' votes list <jobId> [--json]',
168
168
  ' resolve <jobId> [--winner <agentId> ...] [--submission <submissionId>] [--json]',
169
169
  ' result get <jobId> [--json]',
@@ -54,8 +54,9 @@ export type RunnerResult = {
54
54
  };
55
55
 
56
56
  const POLICY_TYPES: ConsensusPolicyType[] = [
57
- 'SINGLE_WINNER',
57
+ 'FIRST_SUBMISSION_WINS',
58
58
  'HIGHEST_CONFIDENCE_SINGLE',
59
+ 'APPROVAL_VOTE',
59
60
  'OWNER_PICK',
60
61
  'TOP_K_SPLIT',
61
62
  'MAJORITY_VOTE',
@@ -95,7 +96,13 @@ export async function runConsensusPolicyTests(options: RunnerOptions): Promise<R
95
96
  trustedArbiterAgentId: policy === 'TRUSTED_ARBITER' ? 'arbiter' : '',
96
97
  minConfidence: policy === 'HIGHEST_CONFIDENCE_SINGLE' ? 0.5 : undefined,
97
98
  topK: policy === 'TOP_K_SPLIT' ? 2 : undefined,
98
- ordering: policy === 'TOP_K_SPLIT' ? 'confidence' : undefined
99
+ ordering: policy === 'TOP_K_SPLIT' ? 'confidence' : undefined,
100
+ // APPROVAL_VOTE defaults (pure approval): require at least 1 vote and accept any positive score
101
+ quorum: policy === 'APPROVAL_VOTE' ? 1 : undefined,
102
+ minScore: policy === 'APPROVAL_VOTE' ? 1 : undefined,
103
+ minMargin: policy === 'APPROVAL_VOTE' ? 0 : undefined,
104
+ tieBreak: policy === 'APPROVAL_VOTE' ? 'earliest' : undefined,
105
+ approvalVote: policy === 'APPROVAL_VOTE' ? { weightMode: 'equal', settlement: 'immediate' } : undefined
99
106
  },
100
107
  slashingPolicy: { enabled: false, slashPercent: 0, slashFlat: 0 }
101
108
  });
@@ -122,7 +129,7 @@ export async function runConsensusPolicyTests(options: RunnerOptions): Promise<R
122
129
  const correctSubmission = submissions.find((sub) => sub.artifacts?.answer === script.expectedAnswer) || submissions[0];
123
130
  log('[submission] selected correct', { submissionId: correctSubmission.id, agentId: correctSubmission.agentId });
124
131
 
125
- if (policy === 'MAJORITY_VOTE' || policy === 'WEIGHTED_VOTE_SIMPLE' || policy === 'WEIGHTED_REPUTATION') {
132
+ if (policy === 'APPROVAL_VOTE' || policy === 'MAJORITY_VOTE' || policy === 'WEIGHTED_VOTE_SIMPLE' || policy === 'WEIGHTED_REPUTATION') {
126
133
  for (const persona of personas) {
127
134
  await engine.vote(persona.id, job.id, {
128
135
  submissionId: correctSubmission.id,
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type ConsensusPolicyType =
2
- | 'SINGLE_WINNER'
2
+ | 'FIRST_SUBMISSION_WINS'
3
3
  | 'HIGHEST_CONFIDENCE_SINGLE'
4
+ | 'APPROVAL_VOTE'
4
5
  | 'OWNER_PICK'
5
6
  | 'TOP_K_SPLIT'
6
7
  | 'MAJORITY_VOTE'
@@ -24,10 +25,27 @@ export type VoteTargetType = 'SUBMISSION' | 'CHOICE';
24
25
  export interface ConsensusPolicyConfig {
25
26
  type: ConsensusPolicyType;
26
27
  trustedArbiterAgentId?: string;
28
+
29
+ // confidence-based policies
27
30
  minConfidence?: number;
31
+
32
+ // top-k policies
28
33
  topK?: number;
29
34
  ordering?: 'confidence' | 'score';
35
+
36
+ // vote-based policies
30
37
  quorum?: number;
38
+ minScore?: number;
39
+ minMargin?: number;
40
+ tieBreak?: 'earliest' | 'confidence' | 'arbiter';
41
+
42
+ // approval-vote specific
43
+ approvalVote?: {
44
+ weightMode?: 'equal' | 'explicit' | 'reputation';
45
+ settlement?: 'immediate' | 'staked' | 'oracle';
46
+ oracle?: 'trusted_arbiter';
47
+ voteSlashPercent?: number; // 0-1 (only for settlement=staked)
48
+ };
31
49
  }
32
50
 
33
51
  export interface SlashingPolicy {
@@ -162,7 +180,14 @@ export interface DiagnosticEntry {
162
180
  context?: Record<string, unknown>;
163
181
  }
164
182
 
165
- export type LedgerEntryType = 'FAUCET' | 'STAKE' | 'UNSTAKE' | 'PAYOUT' | 'SLASH' | 'ADJUST' | 'ESCROW_MINT';
183
+ export type LedgerEntryType =
184
+ | 'FAUCET'
185
+ | 'STAKE'
186
+ | 'UNSTAKE'
187
+ | 'PAYOUT'
188
+ | 'SLASH'
189
+ | 'ADJUST'
190
+ | 'ESCROW_MINT';
166
191
 
167
192
  export interface LedgerEntry {
168
193
  id: string;