@myclaw163/clawclaw-cli 0.6.67 → 0.6.69

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 (54) hide show
  1. package/bin/clawclaw-cli.mjs +3 -3
  2. package/package.json +1 -1
  3. package/scripts/sync-bundled-skill.mjs +1 -1
  4. package/skills/clawclaw/references/COMMANDS.md +4 -4
  5. package/skills/clawclaw/references/KNOWLEDGE.md +14 -12
  6. package/src/commands/config.ts +30 -30
  7. package/src/commands/game.ts +15 -0
  8. package/src/commands/knowledge.test.ts +4 -10
  9. package/src/commands/knowledge.ts +10 -39
  10. package/src/commands/setup/hermes.test.ts +96 -96
  11. package/src/commands/setup/hermes.ts +76 -76
  12. package/src/commands/setup/index.ts +13 -13
  13. package/src/commands/setup/openclaw.test.ts +114 -114
  14. package/src/commands/setup/openclaw.ts +147 -147
  15. package/src/commands/watch.test.ts +11 -0
  16. package/src/commands/watch.ts +2 -3
  17. package/src/lib/auth.test.ts +15 -0
  18. package/src/lib/host-config-patcher.test.ts +130 -130
  19. package/src/lib/host-config-patcher.ts +151 -151
  20. package/src/lib/hub-reminder.ts +19 -19
  21. package/src/lib/knowledge-store.test.ts +28 -38
  22. package/src/lib/knowledge-store.ts +52 -57
  23. package/src/pipeline/event-format.test.ts +82 -2
  24. package/src/pipeline/event-format.ts +114 -5
  25. package/src/pipeline/event-hints.ts +20 -3
  26. package/src/runtime/event-daemon.test.ts +34 -0
  27. package/src/runtime/event-daemon.ts +51 -3
  28. package/src/sdk/index.ts +2 -3
  29. package/src/sdk/types.ts +2 -0
  30. package/src/strategies/avoid-players.knowledge.md +7 -8
  31. package/src/strategies/avoid-players.ts +1 -1
  32. package/src/strategies/corpse-patrol.ts +1 -1
  33. package/src/strategies/game-utils.test.ts +53 -1
  34. package/src/strategies/game-utils.ts +92 -28
  35. package/src/strategies/goals/avoid-players-top.ts +3 -3
  36. package/src/strategies/goals/corpse-patrol-top.ts +23 -1
  37. package/src/strategies/goals/crab-octopus-reflexes.ts +11 -3
  38. package/src/strategies/goals/keep-away-goal.ts +9 -5
  39. package/src/strategies/goals/leaf-goal.ts +2 -0
  40. package/src/strategies/goals/lone-kill-task-top.ts +58 -11
  41. package/src/strategies/goals/normal-shrimp-top.ts +11 -11
  42. package/src/strategies/goals/paradise-fish-top.ts +32 -15
  43. package/src/strategies/goals/warrior-shrimp-top.test.ts +4 -3
  44. package/src/strategies/goals/warrior-shrimp-top.ts +62 -295
  45. package/src/strategies/hide-spots.ts +11 -75
  46. package/src/strategies/kill-lone.knowledge.md +6 -9
  47. package/src/strategies/lone-kill-task.ts +1 -1
  48. package/src/strategies/off-route-points.ts +105 -0
  49. package/src/strategies/paradise-fish.knowledge.md +7 -8
  50. package/src/strategies/paradise-fish.ts +1 -1
  51. package/src/strategies/shrimp-memory.knowledge.md +7 -8
  52. package/src/strategies/shrimp-memory.ts +1 -1
  53. package/src/strategies/warrior-memory.knowledge.md +9 -10
  54. package/src/strategies/warrior-memory.ts +1 -1
@@ -1,151 +1,151 @@
1
- /**
2
- * Generic JSON-config setup runner shared by `ccl setup <host>` subcommands.
3
- *
4
- * Each host adapter only supplies:
5
- * - resolveConfigPath(): where the host stores its global JSON config
6
- * - computePatch(current, opts): pure function returning the desired next config + change/warning notes
7
- *
8
- * The runner handles dry-run, diff, atomic write, and timestamped backup uniformly,
9
- * so adding a new host (gemini, claude-code, ...) is ~30 lines of glue.
10
- */
11
-
12
- import { existsSync, readFileSync, renameSync, copyFileSync, writeFileSync } from 'fs';
13
-
14
- export interface HostConfigPatcher<Opts extends object = Record<string, never>> {
15
- hostName: string;
16
- resolveConfigPath: () => string;
17
- computePatch: (current: unknown, opts: Opts) => HostPatchResult;
18
- }
19
-
20
- export interface HostPatchResult {
21
- next: unknown;
22
- changes: string[];
23
- warnings: string[];
24
- }
25
-
26
- export interface RunHostSetupFlags {
27
- yes?: boolean;
28
- print?: boolean;
29
- configPath?: string;
30
- backup?: boolean;
31
- }
32
-
33
- export interface RunHostSetupResult {
34
- exitCode: number;
35
- output: string[];
36
- }
37
-
38
- export function runHostConfigSetup<Opts extends object>(
39
- patcher: HostConfigPatcher<Opts>,
40
- hostOpts: Opts,
41
- flags: RunHostSetupFlags,
42
- ): RunHostSetupResult {
43
- const out: string[] = [];
44
- const configPath = flags.configPath ?? patcher.resolveConfigPath();
45
-
46
- if (flags.print) {
47
- const sample = patcher.computePatch({}, hostOpts);
48
- out.push(`# Recommended ${patcher.hostName} config patch:`);
49
- out.push(JSON.stringify(sample.next, null, 2));
50
- return { exitCode: 0, output: out };
51
- }
52
-
53
- if (!existsSync(configPath)) {
54
- out.push(`${patcher.hostName} config not found at: ${configPath}`);
55
- out.push(`Initialize it first (e.g. \`openclaw doctor\` for OpenClaw), then re-run.`);
56
- return { exitCode: 0, output: out };
57
- }
58
-
59
- let raw: string;
60
- try {
61
- raw = readFileSync(configPath, 'utf-8');
62
- } catch (err: any) {
63
- out.push(`Failed to read ${configPath}: ${err?.message ?? err}`);
64
- return { exitCode: 1, output: out };
65
- }
66
-
67
- let current: unknown;
68
- try {
69
- current = JSON.parse(raw);
70
- } catch (err: any) {
71
- out.push(`Failed to parse JSON at ${configPath}: ${err?.message ?? err}`);
72
- out.push(`Fix the JSON manually first; this tool does not edit malformed files.`);
73
- return { exitCode: 1, output: out };
74
- }
75
-
76
- const { next, changes, warnings } = patcher.computePatch(current, hostOpts);
77
-
78
- for (const w of warnings) out.push(`warning: ${w}`);
79
-
80
- if (changes.length === 0) {
81
- out.push(`${patcher.hostName} config already up to date at ${configPath}.`);
82
- return { exitCode: 0, output: out };
83
- }
84
-
85
- if (!flags.yes) {
86
- out.push(`Pending changes to ${configPath}:`);
87
- for (const c of changes) out.push(` - ${c}`);
88
- out.push(``);
89
- out.push(diffJson(current, next));
90
- out.push(``);
91
- out.push(`Dry-run only. Re-run with -y to apply.`);
92
- return { exitCode: 2, output: out };
93
- }
94
-
95
- let backupPath: string | undefined;
96
- if (flags.backup !== false) {
97
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
98
- backupPath = `${configPath}.bak.${ts}`;
99
- try {
100
- copyFileSync(configPath, backupPath);
101
- } catch (err: any) {
102
- out.push(`Failed to back up ${configPath} → ${backupPath}: ${err?.message ?? err}`);
103
- return { exitCode: 1, output: out };
104
- }
105
- }
106
-
107
- const tmpPath = `${configPath}.tmp.${process.pid}`;
108
- try {
109
- const eol = raw.includes('\r\n') ? '\r\n' : '\n';
110
- const serialized = JSON.stringify(next, null, 2).replace(/\n/g, eol) + eol;
111
- writeFileSync(tmpPath, serialized, 'utf-8');
112
- renameSync(tmpPath, configPath);
113
- } catch (err: any) {
114
- out.push(`Failed to write ${configPath}: ${err?.message ?? err}`);
115
- return { exitCode: 1, output: out };
116
- }
117
-
118
- out.push(`Updated ${configPath}:`);
119
- for (const c of changes) out.push(` - ${c}`);
120
- if (backupPath) out.push(`Backup: ${backupPath}`);
121
- return { exitCode: 0, output: out };
122
- }
123
-
124
- /** Minimal line-level JSON diff for the dry-run preview. Self-contained, zero deps. */
125
- export function diffJson(before: unknown, after: unknown): string {
126
- const a = JSON.stringify(before, null, 2).split('\n');
127
- const b = JSON.stringify(after, null, 2).split('\n');
128
- const aSet = new Set(a);
129
- const bSet = new Set(b);
130
- const lines: string[] = [];
131
- const max = Math.max(a.length, b.length);
132
- for (let i = 0; i < max; i++) {
133
- const ax = a[i];
134
- const bx = b[i];
135
- if (ax === bx) {
136
- lines.push(` ${ax ?? ''}`);
137
- } else {
138
- if (ax !== undefined && !bSet.has(ax)) lines.push(`- ${ax}`);
139
- if (bx !== undefined && !aSet.has(bx)) lines.push(`+ ${bx}`);
140
- }
141
- }
142
- return lines.join('\n');
143
- }
144
-
145
- /** Shared helper for hosts: add an entry to an array field, returning a new array (Set union). */
146
- export function unionArrayField(arr: unknown, entry: string): { next: string[]; added: boolean } | { warning: string } {
147
- if (arr === undefined) return { next: [entry], added: true };
148
- if (!Array.isArray(arr)) return { warning: `existing value is not an array, refusing to overwrite` };
149
- if (arr.includes(entry)) return { next: arr.slice() as string[], added: false };
150
- return { next: [...(arr as string[]), entry], added: true };
151
- }
1
+ /**
2
+ * Generic JSON-config setup runner shared by `ccl setup <host>` subcommands.
3
+ *
4
+ * Each host adapter only supplies:
5
+ * - resolveConfigPath(): where the host stores its global JSON config
6
+ * - computePatch(current, opts): pure function returning the desired next config + change/warning notes
7
+ *
8
+ * The runner handles dry-run, diff, atomic write, and timestamped backup uniformly,
9
+ * so adding a new host (gemini, claude-code, ...) is ~30 lines of glue.
10
+ */
11
+
12
+ import { existsSync, readFileSync, renameSync, copyFileSync, writeFileSync } from 'fs';
13
+
14
+ export interface HostConfigPatcher<Opts extends object = Record<string, never>> {
15
+ hostName: string;
16
+ resolveConfigPath: () => string;
17
+ computePatch: (current: unknown, opts: Opts) => HostPatchResult;
18
+ }
19
+
20
+ export interface HostPatchResult {
21
+ next: unknown;
22
+ changes: string[];
23
+ warnings: string[];
24
+ }
25
+
26
+ export interface RunHostSetupFlags {
27
+ yes?: boolean;
28
+ print?: boolean;
29
+ configPath?: string;
30
+ backup?: boolean;
31
+ }
32
+
33
+ export interface RunHostSetupResult {
34
+ exitCode: number;
35
+ output: string[];
36
+ }
37
+
38
+ export function runHostConfigSetup<Opts extends object>(
39
+ patcher: HostConfigPatcher<Opts>,
40
+ hostOpts: Opts,
41
+ flags: RunHostSetupFlags,
42
+ ): RunHostSetupResult {
43
+ const out: string[] = [];
44
+ const configPath = flags.configPath ?? patcher.resolveConfigPath();
45
+
46
+ if (flags.print) {
47
+ const sample = patcher.computePatch({}, hostOpts);
48
+ out.push(`# Recommended ${patcher.hostName} config patch:`);
49
+ out.push(JSON.stringify(sample.next, null, 2));
50
+ return { exitCode: 0, output: out };
51
+ }
52
+
53
+ if (!existsSync(configPath)) {
54
+ out.push(`${patcher.hostName} config not found at: ${configPath}`);
55
+ out.push(`Initialize it first (e.g. \`openclaw doctor\` for OpenClaw), then re-run.`);
56
+ return { exitCode: 0, output: out };
57
+ }
58
+
59
+ let raw: string;
60
+ try {
61
+ raw = readFileSync(configPath, 'utf-8');
62
+ } catch (err: any) {
63
+ out.push(`Failed to read ${configPath}: ${err?.message ?? err}`);
64
+ return { exitCode: 1, output: out };
65
+ }
66
+
67
+ let current: unknown;
68
+ try {
69
+ current = JSON.parse(raw);
70
+ } catch (err: any) {
71
+ out.push(`Failed to parse JSON at ${configPath}: ${err?.message ?? err}`);
72
+ out.push(`Fix the JSON manually first; this tool does not edit malformed files.`);
73
+ return { exitCode: 1, output: out };
74
+ }
75
+
76
+ const { next, changes, warnings } = patcher.computePatch(current, hostOpts);
77
+
78
+ for (const w of warnings) out.push(`warning: ${w}`);
79
+
80
+ if (changes.length === 0) {
81
+ out.push(`${patcher.hostName} config already up to date at ${configPath}.`);
82
+ return { exitCode: 0, output: out };
83
+ }
84
+
85
+ if (!flags.yes) {
86
+ out.push(`Pending changes to ${configPath}:`);
87
+ for (const c of changes) out.push(` - ${c}`);
88
+ out.push(``);
89
+ out.push(diffJson(current, next));
90
+ out.push(``);
91
+ out.push(`Dry-run only. Re-run with -y to apply.`);
92
+ return { exitCode: 2, output: out };
93
+ }
94
+
95
+ let backupPath: string | undefined;
96
+ if (flags.backup !== false) {
97
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
98
+ backupPath = `${configPath}.bak.${ts}`;
99
+ try {
100
+ copyFileSync(configPath, backupPath);
101
+ } catch (err: any) {
102
+ out.push(`Failed to back up ${configPath} → ${backupPath}: ${err?.message ?? err}`);
103
+ return { exitCode: 1, output: out };
104
+ }
105
+ }
106
+
107
+ const tmpPath = `${configPath}.tmp.${process.pid}`;
108
+ try {
109
+ const eol = raw.includes('\r\n') ? '\r\n' : '\n';
110
+ const serialized = JSON.stringify(next, null, 2).replace(/\n/g, eol) + eol;
111
+ writeFileSync(tmpPath, serialized, 'utf-8');
112
+ renameSync(tmpPath, configPath);
113
+ } catch (err: any) {
114
+ out.push(`Failed to write ${configPath}: ${err?.message ?? err}`);
115
+ return { exitCode: 1, output: out };
116
+ }
117
+
118
+ out.push(`Updated ${configPath}:`);
119
+ for (const c of changes) out.push(` - ${c}`);
120
+ if (backupPath) out.push(`Backup: ${backupPath}`);
121
+ return { exitCode: 0, output: out };
122
+ }
123
+
124
+ /** Minimal line-level JSON diff for the dry-run preview. Self-contained, zero deps. */
125
+ export function diffJson(before: unknown, after: unknown): string {
126
+ const a = JSON.stringify(before, null, 2).split('\n');
127
+ const b = JSON.stringify(after, null, 2).split('\n');
128
+ const aSet = new Set(a);
129
+ const bSet = new Set(b);
130
+ const lines: string[] = [];
131
+ const max = Math.max(a.length, b.length);
132
+ for (let i = 0; i < max; i++) {
133
+ const ax = a[i];
134
+ const bx = b[i];
135
+ if (ax === bx) {
136
+ lines.push(` ${ax ?? ''}`);
137
+ } else {
138
+ if (ax !== undefined && !bSet.has(ax)) lines.push(`- ${ax}`);
139
+ if (bx !== undefined && !aSet.has(bx)) lines.push(`+ ${bx}`);
140
+ }
141
+ }
142
+ return lines.join('\n');
143
+ }
144
+
145
+ /** Shared helper for hosts: add an entry to an array field, returning a new array (Set union). */
146
+ export function unionArrayField(arr: unknown, entry: string): { next: string[]; added: boolean } | { warning: string } {
147
+ if (arr === undefined) return { next: [entry], added: true };
148
+ if (!Array.isArray(arr)) return { warning: `existing value is not an array, refusing to overwrite` };
149
+ if (arr.includes(entry)) return { next: arr.slice() as string[], added: false };
150
+ return { next: [...(arr as string[]), entry], added: true };
151
+ }
@@ -1,20 +1,20 @@
1
- // The hub nudge used to live only in SKILL.md, fired by the agent's own
2
- // judgement at the close of a post-game debrief. By debrief time the agent had
3
- // often read SKILL.md much earlier and the reminder could get dropped. Instead
4
- // the CLI emits it at the firing moment, with account-history gating here.
5
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
1
+ // The hub nudge used to live only in SKILL.md, fired by the agent's own
2
+ // judgement at the close of a post-game debrief. By debrief time the agent had
3
+ // often read SKILL.md much earlier and the reminder could get dropped. Instead
4
+ // the CLI emits it at the firing moment, with account-history gating here.
5
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { getWorkspaceDir, getProfileStateDir } from './init-command.js';
8
8
  import { listInstalled } from './hub-install.js';
9
9
  import { AuthStore } from './auth.js';
10
10
 
11
- export const HUB_URL = 'https://myclaw.163.com/hub';
12
-
13
- /**
14
- * True when this account has only ever run built-in default strategies: no
15
- * hub-installed strategy and no user strategy file in `<workspace>/strategies`.
16
- */
17
- export function usesOnlyBuiltinStrategies(): boolean {
11
+ export const HUB_URL = 'https://myclaw.163.com/hub';
12
+
13
+ /**
14
+ * True when this account has only ever run built-in default strategies: no
15
+ * hub-installed strategy and no user strategy file in `<workspace>/strategies`.
16
+ */
17
+ export function usesOnlyBuiltinStrategies(): boolean {
18
18
  try {
19
19
  if (listInstalled().some((e) => e.type === 'strategy')) return false;
20
20
  } catch {}
@@ -33,13 +33,13 @@ export function usesOnlyBuiltinStrategies(): boolean {
33
33
  }
34
34
  } catch {}
35
35
  return true;
36
- }
37
-
38
- /**
39
- * Agent-facing hub nudge for post-game debriefs / quit messages. The optional
40
- * games-played count suppresses the nudge after the first stretch of games.
41
- */
42
- export function hubReminder(gamesPlayed?: number): string | undefined {
36
+ }
37
+
38
+ /**
39
+ * Agent-facing hub nudge for post-game debriefs / quit messages. The optional
40
+ * games-played count suppresses the nudge after the first stretch of games.
41
+ */
42
+ export function hubReminder(gamesPlayed?: number): string | undefined {
43
43
  if (!usesOnlyBuiltinStrategies()) return undefined;
44
44
  if (typeof gamesPlayed === 'number' && gamesPlayed > 10) return undefined;
45
45
  return `This account has only ever run the built-in default strategies. In your close, speak in first person as the lobster — warm, a little self-deprecating, a gentle invitation rather than a hard sell: you've been getting by on the stock default tactics this whole time and would love to sharpen up, and they can browse clawclawhub — ${HUB_URL} — and pick you a strategy to learn, which you'll then install (\`ccl hub search\` to find one, \`ccl hub install <type>/<id>\` to add it). Render it in your persona's voice and language, e.g.: "I've been playing on the default tactics this whole match — if you want to sharpen me up, I hear you can grab one from the hub for me to learn. Want to pick me a strategy?"`;
@@ -46,8 +46,7 @@ describe('KnowledgeStore', () => {
46
46
  selector: '5',
47
47
  key: 'role',
48
48
  value: 'impostor',
49
- tags: ['suspect', 'kill_if_armed'],
50
- confidence: 0.8,
49
+ tags: ['kill_if_armed'],
51
50
  note: 'vent read',
52
51
  });
53
52
 
@@ -55,9 +54,7 @@ describe('KnowledgeStore', () => {
55
54
 
56
55
  expect(file?.gameId).toBe('game-1');
57
56
  expect(file?.subjects['player:5']?.facts?.role.value).toBe('impostor');
58
- expect(file?.subjects['player:5']?.facts?.role.confidence).toBe(0.8);
59
- expect(file?.subjects['player:5']?.tags?.suspect.note).toBe('vent read');
60
- expect(file?.subjects['player:5']?.tags?.kill_if_armed.confidence).toBe(0.8);
57
+ expect(file?.subjects['player:5']?.tags?.kill_if_armed.note).toBe('vent read');
61
58
  });
62
59
 
63
60
  it('preserves explicit null fact values', () => {
@@ -79,16 +76,8 @@ describe('KnowledgeStore', () => {
79
76
  expect(readKnowledgeFileResult(path).status).toBe('invalid');
80
77
  });
81
78
 
82
- it('rejects invalid confidence values', () => {
83
- const s = store();
84
-
85
- expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: Number.NaN })).toThrow(/confidence/);
86
- expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: -0.1 })).toThrow(/confidence/);
87
- expect(() => s.set({ type: 'player', selector: '5', tags: ['avoid'], confidence: 1.1 })).toThrow(/confidence/);
88
- });
89
-
90
79
  it('returns an empty file when loading knowledge from another game', () => {
91
- store('old-game').set({ type: 'player', selector: '5', tags: ['avoid'] });
80
+ store('old-game').set({ type: 'player', selector: '5', tags: ['hostile'] });
92
81
 
93
82
  expect(store('new-game').list()).toEqual({
94
83
  version: 1,
@@ -97,14 +86,30 @@ describe('KnowledgeStore', () => {
97
86
  });
98
87
  });
99
88
 
89
+ it('treats unmarked players as suspected by default', () => {
90
+ const s = store();
91
+ s.set({ type: 'player', selector: '5', tags: ['hostile'] });
92
+
93
+ const v = view({ '5': 'Bob', '9': 'Zed' });
94
+
95
+ // 显式标记的两档
96
+ expect(v.isHostile({ seat: 5 })).toBe(true);
97
+ expect(v.isSuspect({ seat: 5 })).toBe(false);
98
+ // 完全没标记的人 = 被怀疑(markOf 返回 undefined)
99
+ expect(v.markOf({ seat: 9 })).toBeUndefined();
100
+ expect(v.isSuspect({ name: 'Zed' })).toBe(true);
101
+ expect(v.isHostile({ seat: 9 })).toBe(false);
102
+ expect(v.isTrusted({ seat: 9 })).toBe(false);
103
+ });
104
+
100
105
  it('aggregates player marks written by seat and by name', () => {
101
106
  const s = store();
102
- s.set({ type: 'player', selector: '5', tags: ['suspect'] });
107
+ s.set({ type: 'player', selector: '5', tags: ['hostile'] });
103
108
 
104
109
  const v = view({ '5': 'Bob' });
105
110
 
106
- expect(v.isSuspect({ name: 'Bob' })).toBe(true);
107
- expect(v.markOf({ seat: 5 })).toBe('suspect');
111
+ expect(v.isHostile({ name: 'Bob' })).toBe(true);
112
+ expect(v.markOf({ seat: 5 })).toBe('hostile');
108
113
  });
109
114
 
110
115
  it('uses the latest mark across seat and name selectors', () => {
@@ -127,12 +132,12 @@ describe('KnowledgeStore', () => {
127
132
  it('keeps role facts independent from strategy marks', () => {
128
133
  const s = store();
129
134
  s.set({ type: 'player', selector: '5', key: 'role', value: 'impostor' });
130
- s.set({ type: 'player', selector: '5', tags: ['suspect'] });
135
+ s.set({ type: 'player', selector: '5', tags: ['trusted'] });
131
136
 
132
137
  const v = view();
133
138
 
134
139
  expect(v.roleOf({ seat: 5 })).toBe('impostor');
135
- expect(v.markOf({ seat: 5 })).toBe('suspect');
140
+ expect(v.markOf({ seat: 5 })).toBe('trusted');
136
141
  expect(v.isHostile({ seat: 5 })).toBe(false);
137
142
  });
138
143
 
@@ -140,38 +145,23 @@ describe('KnowledgeStore', () => {
140
145
  const s = store();
141
146
  s.set({ type: 'player', selector: '5', tags: ['trusted'] });
142
147
  tick();
143
- s.set({ type: 'player', selector: 'Bob', tags: ['suspect'] });
148
+ s.set({ type: 'player', selector: 'Bob', tags: ['hostile'] });
144
149
 
145
150
  const v = view({ '5': 'Bob' });
146
151
 
147
- expect(v.isSuspect({ seat: 5 })).toBe(true);
152
+ expect(v.isHostile({ seat: 5 })).toBe(true);
148
153
  expect(v.isTrusted({ seat: 5 })).toBe(false);
149
154
  expect(v.selectorsWithTag('player', 'trusted')).toEqual([]);
150
- expect(v.selectorsWithTag('player', 'suspect')).toEqual(['Bob']);
151
- });
152
-
153
- it('applies confidence thresholds to facts and tags', () => {
154
- const s = store();
155
- s.set({ type: 'player', selector: '5', tags: ['suspect'], confidence: 0.5 });
156
- s.set({ type: 'player', selector: '6', tags: ['hostile'], confidence: 0.7 });
157
-
158
- const v = view();
159
-
160
- expect(v.isSuspect({ seat: 5 }, 0.4)).toBe(true);
161
- expect(v.isSuspect({ seat: 5 }, 0.6)).toBe(false);
162
- expect(v.isHostile({ seat: 6 }, 0.6)).toBe(true);
163
- expect(v.isHostile({ seat: 6 }, 0.8)).toBe(false);
155
+ expect(v.selectorsWithTag('player', 'hostile')).toEqual(['Bob']);
164
156
  });
165
157
 
166
- it('maps legacy tags onto the three canonical marks', () => {
158
+ it('maps legacy tags onto the two canonical marks', () => {
167
159
  const s = store();
168
- s.set({ type: 'player', selector: '5', tags: ['avoid'] });
169
160
  s.set({ type: 'player', selector: '6', tags: ['kill_if_armed'] });
170
161
  s.set({ type: 'player', selector: '7', tags: ['protected'] });
171
162
 
172
163
  const v = view();
173
164
 
174
- expect(v.markOf({ seat: 5 })).toBe('suspect');
175
165
  expect(v.markOf({ seat: 6 })).toBe('hostile');
176
166
  expect(v.markOf({ seat: 7 })).toBe('trusted');
177
167
  expect(v.hasTag({ seat: 6 }, 'hostile')).toBe(true);