@cairnkeep/cli 2.24.0 → 2.25.0

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 (48) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/docs/README.md +4 -0
  3. package/docs/agents.md +18 -0
  4. package/docs/design/team-mode.md +17 -9
  5. package/docs/harness-compatibility.md +17 -0
  6. package/docs/learning/CURRICULUM-MAP.md +3 -1
  7. package/docs/learning/FEATURE-GUIDE.md +29 -3
  8. package/docs/learning/README.md +5 -3
  9. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  10. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  11. package/docs/learning/lessons/L02-installation.md +2 -2
  12. package/docs/learning/lessons/L03-first-project.md +1 -1
  13. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  14. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  15. package/docs/learning/lessons/L21-context-packs.md +1 -1
  16. package/docs/learning/lessons/L22-native-windows.md +2 -2
  17. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  18. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  19. package/docs/learning/lessons/L25-playbooks.md +1 -1
  20. package/docs/learning/lessons/L26-context-intelligence.md +1 -1
  21. package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
  22. package/docs/learning/lessons/L28-security-assurance.md +2 -2
  23. package/docs/learning/lessons/L29-team-continuity.md +95 -0
  24. package/docs/learning/tracks/operator.md +2 -0
  25. package/docs/learning/video-scripts/V29-team-continuity.md +66 -0
  26. package/docs/operating.md +17 -3
  27. package/docs/privacy-and-data-flow.md +24 -5
  28. package/docs/security-assurance.md +28 -0
  29. package/docs/storage.md +18 -0
  30. package/docs/team-pilot.md +132 -0
  31. package/docs/team.md +234 -0
  32. package/examples/eval/bundled-fake.json +1 -1
  33. package/mcp-memory-server/dist/team-cli.js +218 -0
  34. package/mcp-memory-server/dist/team-http.js +193 -0
  35. package/mcp-memory-server/dist/team-mcp.js +82 -0
  36. package/mcp-memory-server/dist/team-store.js +663 -0
  37. package/package.json +1 -1
  38. package/scripts/cairn-cli.mjs +2 -0
  39. package/scripts/completion.sh +23 -3
  40. package/scripts/lib/team-retention.mjs +29 -0
  41. package/scripts/test-ci-sharding.sh +4 -0
  42. package/scripts/test-completion.sh +8 -0
  43. package/scripts/test-package-install.sh +9 -0
  44. package/scripts/test-team-docs.sh +52 -0
  45. package/scripts/test-team-pilot.mjs +59 -0
  46. package/scripts/uninstall.sh +13 -0
  47. package/scripts/verify-team-retention.mjs +3 -0
  48. package/scripts/windows-platform.mjs +6 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.24.0",
3
+ "version": "2.25.0",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -21,6 +21,7 @@ const NODE_COMMANDS = new Map([
21
21
  ["evidence", "work-evidence-cli.js"],
22
22
  ["playbook", "playbook-cli.js"],
23
23
  ["security", "security-cli.js"],
24
+ ["team", "team-cli.js"],
24
25
  ]);
25
26
 
26
27
  const POSIX_COMMANDS = new Map([
@@ -61,6 +62,7 @@ Usage:
61
62
  cairn evidence <list|show|delete|prune|doctor>
62
63
  cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>
63
64
  cairn security doctor [--project PATH] [--json]
65
+ cairn team <init|project|member|credential|serve|mcp|list|read|search|history|propose|proposals|proposal-show|review|audit|doctor|backup|restore|memory-delete>
64
66
  cairn memory <path|export|import>
65
67
  cairn audit-timer [--on-calendar SPEC] [--para-root PATH] [--render-only DIR]
66
68
  cairn uninstall [--dry-run] [--yes] [--purge-memory] [--purge-packs] [PROJECT ...]
@@ -13,7 +13,7 @@ _cairn_complete() {
13
13
  COMPREPLY=()
14
14
  current=${COMP_WORDS[COMP_CWORD]}
15
15
  previous=${COMP_WORDS[COMP_CWORD-1]:-}
16
- commands="bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
16
+ commands="bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security team trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
17
17
  if (( COMP_CWORD == 1 )); then
18
18
  COMPREPLY=( $(compgen -W "$commands" -- "$current") )
19
19
  return
@@ -27,6 +27,7 @@ _cairn_complete() {
27
27
  sync-omp) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
28
28
  doctor) COMPREPLY=( $(compgen -W "--repair" -- "$current") ) ;;
29
29
  security) COMPREPLY=( $(compgen -W "doctor --project --json" -- "$current") ) ;;
30
+ team) COMPREPLY=( $(compgen -W "init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete --organization --data --project --roles --class --expires-at --output --input --confirm --port --key --query --proposal --decision --revision --project-root" -- "$current") ) ;;
30
31
  trajectory) COMPREPLY=( $(compgen -W "list show prune --json --dry-run" -- "$current") ) ;;
31
32
  artifact) COMPREPLY=( $(compgen -W "list show delete prune --kind --session --json --dry-run --include-protected" -- "$current") ) ;;
32
33
  evidence) COMPREPLY=( $(compgen -W "list show delete prune doctor --status --json --dry-run --repair" -- "$current") ) ;;
@@ -100,6 +101,7 @@ _cairn() {
100
101
  'sync-omp:install the oh-my-pi memory extension'
101
102
  'doctor:check runtime dependencies and endpoints'
102
103
  'security:audit local security posture'
104
+ 'team:operate opt-in reviewed team continuity'
103
105
  'trajectory:inspect and prune local session trajectories'
104
106
  'artifact:inspect, delete, and prune local artifacts'
105
107
  'evidence:inspect and manage local Git-linked work evidence'
@@ -132,6 +134,7 @@ _cairn() {
132
134
  sync-omp) _arguments '--apply[apply changes]' '--live-root[omp agent root]:directory:_files -/' ;;
133
135
  doctor) _arguments '--repair[repair trajectory metadata and indexes]' ;;
134
136
  security) _values 'security command' doctor '--project[project root]:directory:_files -/' '--json[emit JSON]' ;;
137
+ team) _values 'team command' init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete '--organization[team option]:value:' '--data[team option]:value:' '--project[team option]:value:' '--roles[team option]:value:' '--class[team option]:value:' '--expires-at[team option]:value:' '--output[team option]:value:' '--input[team option]:value:' '--confirm[team option]:value:' '--port[team option]:value:' '--key[team option]:value:' '--query[team option]:value:' '--proposal[team option]:value:' '--decision[team option]:value:' '--revision[team option]:value:' '--project-root[team option]:value:' ;;
135
138
  trajectory) _values 'trajectory command' list show prune ;;
136
139
  artifact)
137
140
  case $words[3] in
@@ -196,8 +199,8 @@ EOF
196
199
  fish)
197
200
  sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
198
201
  complete -c cairn -f
199
- for command in bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
200
- complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
202
+ for command in bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security team trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
203
+ complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor security team trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
201
204
  end
202
205
  complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
203
206
  complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
@@ -219,6 +222,23 @@ complete -c cairn -n "__fish_seen_subcommand_from doctor" -l repair
219
222
  complete -c cairn -n "__fish_seen_subcommand_from security" -a "doctor"
220
223
  complete -c cairn -n "__fish_seen_subcommand_from security" -l project -r
221
224
  complete -c cairn -n "__fish_seen_subcommand_from security" -l json
225
+ complete -c cairn -n "__fish_seen_subcommand_from team" -a "init project member credential create set remove issue revoke serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete"
226
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l organization -r
227
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l data -r
228
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l project -r
229
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l roles -r
230
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l class -r
231
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l expires-at -r
232
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l output -r
233
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l input -r
234
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l confirm -r
235
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l port -r
236
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l key -r
237
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l query -r
238
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l proposal -r
239
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l decision -r
240
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l revision -r
241
+ complete -c cairn -n "__fish_seen_subcommand_from team" -l project-root -r
222
242
  complete -c cairn -n "__fish_seen_subcommand_from trajectory" -a "list show prune"
223
243
  complete -c cairn -n "__fish_seen_subcommand_from artifact" -a "list show delete prune"
224
244
  complete -c cairn -n "__fish_seen_subcommand_from artifact; and __fish_seen_subcommand_from list" -l kind -r
@@ -0,0 +1,29 @@
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, join, resolve, sep } from 'node:path';
4
+
5
+ function canonical(path) {
6
+ const absolute = resolve(path);
7
+ if (existsSync(absolute)) return realpathSync(absolute);
8
+ const parent = dirname(absolute);
9
+ return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
10
+ }
11
+
12
+ /** Uninstall never grants permission to purge the separate team store. */
13
+ export function assertUninstallRetainsTeam(targets, env = process.env, home = homedir()) {
14
+ const stores = new Set([join(home, '.cairnkeep', 'team'), ...(env.CAIRN_TEAM_BASE_DIR ? [env.CAIRN_TEAM_BASE_DIR] : [])]);
15
+ const normalized = path => {
16
+ const value = canonical(path);
17
+ return process.platform === 'win32' ? value.toLowerCase() : value;
18
+ };
19
+ for (const team of stores) {
20
+ if (!existsSync(resolve(team))) continue;
21
+ const protectedPath = normalized(team);
22
+ for (const target of targets) {
23
+ const path = normalized(target);
24
+ if (path === protectedPath || protectedPath.startsWith(path.endsWith(sep) ? path : `${path}${sep}`)) {
25
+ throw new Error('Uninstall refused: a removal target contains team data. Retain it, or relocate the team store with an explicitly verified backup first.');
26
+ }
27
+ }
28
+ }
29
+ }
@@ -17,6 +17,10 @@ assert.equal(flattened.length, smokeChecks.length);
17
17
  assert.equal(new Set(flattened).size, smokeChecks.length);
18
18
  assert.deepEqual([...flattened].sort(), smokeChecks.map(({ name }) => name).sort());
19
19
  assert.ok(plan.every(({ checks }) => checks.length > 0));
20
+ const teamContracts = ['team', 'team-review', 'team-http', 'team-mcp', 'team-restore', 'team-cli'];
21
+ for (const name of teamContracts) assert.ok(flattened.includes(name), `missing team contract: ${name}`);
22
+ assert.ok(plan.every(({ checks }) => checks.some(({ name }) => teamContracts.includes(name))),
23
+ 'native ACL-heavy team contracts must be distributed across all Windows shards');
20
24
  const weights = plan.map(({ weight }) => weight);
21
25
  assert.ok(Math.max(...weights) - Math.min(...weights) <= 5,
22
26
  `native Windows shards are imbalanced: ${weights.join(", ")}`);
@@ -17,6 +17,11 @@ eval_flags='--task-set --adapter --output --repetitions --seed --json --yes --di
17
17
  skill_commands='harvest list show review propose evaluate apply rollback'
18
18
  for shell in bash zsh fish; do
19
19
  grep -q 'capabilities' "$tmp/$shell"
20
+ grep -q 'team' "$tmp/$shell"
21
+ grep -q 'credential create set remove issue revoke' "$tmp/$shell"
22
+ for team_command in init project member credential serve mcp list read search history propose proposals proposal-show review audit doctor backup restore memory-delete; do
23
+ grep -qF "$team_command" "$tmp/$shell"
24
+ done
20
25
  grep -q 'list.*status.*enable.*disable.*reset.*logging\|list status enable disable reset logging' "$tmp/$shell"
21
26
  grep -q -- '--json\|-l json' "$tmp/$shell"
22
27
  for capability_id in $capability_ids; do
@@ -152,6 +157,9 @@ for shell in bash zsh fish; do
152
157
  fi
153
158
  done
154
159
  "$ROOT/bin/cairn" completion powershell >"$tmp/powershell"
160
+ grep -qF "'team'" "$tmp/powershell"
161
+ grep -qF "'credential','create','set','remove','issue','revoke'" "$tmp/powershell"
162
+ grep -qF "'memory-delete'" "$tmp/powershell"
155
163
  grep -qF "'protocol'" "$tmp/powershell"
156
164
  grep -qF "'--trajectory'" "$tmp/powershell"
157
165
  grep -qF "'--codex-jsonl'" "$tmp/powershell"
@@ -60,6 +60,15 @@ env -u CAIRN_NOTE_DISTILLATION cairn notes --help >/dev/null \
60
60
  || fail "installed package omitted the notes command"
61
61
 
62
62
  installed_root="$tmp/prefix/lib/node_modules/@cairnkeep/cli"
63
+ cairn team --help >"$tmp/installed-team-help"
64
+ grep -qF 'experimental token-first team continuity' "$tmp/installed-team-help" || fail "installed package omitted team preview"
65
+ CAIRN_TEAM=1 cairn team init --organization demo-org --data "$tmp/installed-team" >"$tmp/team-init.json"
66
+ CAIRN_TEAM=1 cairn team project create alpha --data "$tmp/installed-team" >/dev/null
67
+ CAIRN_TEAM=1 cairn team backup --data "$tmp/installed-team" --output "$tmp/team-snapshot.json" >"$tmp/team-backup.json"
68
+ snapshot_digest=$(node -p 'require(process.argv[1]).snapshot_digest' "$tmp/team-backup.json")
69
+ CAIRN_TEAM=1 cairn team restore --input "$tmp/team-snapshot.json" --confirm "$snapshot_digest" --data "$tmp/installed-team-restored" >"$tmp/team-restore.json"
70
+ CAIRN_TEAM=1 cairn team doctor --data "$tmp/installed-team-restored" >"$tmp/team-doctor.json"
71
+ node -e 'if(!require(process.argv[1]).credentials_revoked||!require(process.argv[2]).ok)process.exit(1)' "$tmp/team-restore.json" "$tmp/team-doctor.json" || fail "installed team restore contract failed"
63
72
  cairn skill --help >"$tmp/installed-skill-help"
64
73
  cmp -s "$tmp/source-skill-help" "$tmp/installed-skill-help" || \
65
74
  fail "source and installed skill help differ"
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
4
+ cd "$ROOT"
5
+ lesson=docs/learning/lessons/L29-team-continuity.md
6
+ video=docs/learning/video-scripts/V29-team-continuity.md
7
+ for doc in docs/team.md "$lesson" "$video"; do
8
+ [[ -f "$doc" ]]
9
+ grep -qi 'preview' "$doc"
10
+ grep -qi 'independent.*review' "$doc"
11
+ grep -qi 'pilot' "$doc"
12
+ if ! grep -qF '2.25.0' "$doc"; then
13
+ echo "Missing team preview release compatibility: $doc" >&2
14
+ exit 1
15
+ fi
16
+ if grep -qiE 'unreleased|feature-branch preview|released Cairnkeep CLI does not include' "$doc"; then
17
+ echo 'Released team preview guidance must not claim the feature is absent' >&2
18
+ exit 1
19
+ fi
20
+ done
21
+ [[ -f docs/team-pilot.md ]]
22
+ grep -qF 'team-pilot.md' docs/team.md
23
+ grep -qF 'team-pilot.md' "$lesson"
24
+ grep -qF 'team-pilot.md' "$video"
25
+ grep -qF 'not production admission' docs/team-pilot.md
26
+ grep -qF 'reviewer.token' docs/team-pilot.md
27
+ if grep -qF 'released 2.22.5 baseline' docs/README.md || grep -qF 'Cairnkeep 2.22.5 baseline' "$lesson"; then
28
+ echo 'Team guidance must not pin an obsolete released baseline' >&2
29
+ exit 1
30
+ fi
31
+ grep -q '^\*\*Status:\*\* Brief' "$lesson"
32
+ grep -q '^## Acceptance criteria$' "$lesson"
33
+ grep -q '^## Privacy and trust boundary$' "$lesson"
34
+ grep -qF 'L29-team-continuity.md' docs/learning/README.md
35
+ grep -qF 'L29-team-continuity.md' docs/learning/tracks/operator.md
36
+ grep -qF '`cairn team`' docs/learning/CURRICULUM-MAP.md
37
+ for command in init project member credential serve mcp proposals proposal-show propose review search read audit doctor backup restore memory-delete; do
38
+ node mcp-memory-server/dist/team-cli.js --help | grep -q "cairn team $command"
39
+ done
40
+ node mcp-memory-server/dist/team-cli.js --help | grep -qF 'member remove SUBJECT --project ID'
41
+ for doc in docs/team.md "$lesson" "$video"; do
42
+ grep -qF 'cairn team member remove' "$doc"
43
+ done
44
+ for guide in operating storage privacy-and-data-flow security-assurance harness-compatibility agents; do
45
+ grep -qF 'team.md' "docs/$guide.md"
46
+ done
47
+ for control in team team-review team-http team-mcp team-restore team-cli; do
48
+ grep -qF "smoke-$control.mjs" "$lesson"
49
+ [[ -f "mcp-memory-server/scripts/smoke-$control.mjs" ]]
50
+ done
51
+ node scripts/test-team-pilot.mjs
52
+ echo 'PASS: team preview reference, source controls, admission boundary, learning and video routing'
@@ -0,0 +1,59 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
5
+ import { homedir, tmpdir } from 'node:os';
6
+ import { dirname, join, resolve } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { readStableFile } from './lib/stable-file.mjs';
9
+
10
+ if (process.platform === 'win32') {
11
+ console.log('SKIP: isolated pilot Bash recipe is Linux/macOS; native CLI controls run separately');
12
+ process.exit(0);
13
+ }
14
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
+ const guide = readFileSync(join(root, 'docs/team-pilot.md'), 'utf8');
16
+ const blocks = [...guide.matchAll(/<!-- team-pilot-setup:start -->\s*```sh\n([\s\S]*?)```\s*<!-- team-pilot-setup:end -->/g)];
17
+ assert.equal(blocks.length, 1, 'exactly one maintained executable setup block');
18
+ const hash = bytes => createHash('sha256').update(bytes).digest('hex');
19
+ const productionPaths = ['.codex/config.toml', '.claude.json', '.qwen/settings.json',
20
+ '.qwen/trustedFolders.json', '.config/opencode/opencode.json'];
21
+ const fingerprint = () => productionPaths.map(name => {
22
+ const path = join(homedir(), name);
23
+ return existsSync(path) ? hash(readFileSync(path)) : null;
24
+ });
25
+ const before = fingerprint();
26
+ const base = mkdtempSync(join(tmpdir(), 'cairn-team-pilot-doc-'));
27
+ const pilot = join(base, 'new lab');
28
+ const environment = { PATH: process.env.PATH, LANG: 'C.UTF-8',
29
+ TEAM_CAIRN: join(root, 'bin/cairn'), TEAM_PILOT_DIR: pilot };
30
+ try {
31
+ const readToken = name => readStableFile(join(pilot, name), {
32
+ label: 'Pilot credential', maxBytes: 256, privateMode: 0o600,
33
+ }).bytes;
34
+ const execute = () => spawnSync('bash', ['-c', blocks[0][1]], {
35
+ cwd: base, env: environment, encoding: 'utf8', timeout: 120000,
36
+ });
37
+ const result = execute();
38
+ assert.equal(result.error, undefined);
39
+ assert.equal(result.status, 0, result.stderr);
40
+ assert.equal(statSync(pilot).mode & 0o777, 0o700);
41
+ const files = ['author.token', 'reader.token', 'reviewer.token'];
42
+ const hashes = files.map(name => {
43
+ const bytes = readToken(name);
44
+ const token = bytes.toString('utf8').trim();
45
+ assert.equal(result.stdout.includes(token), false, 'credential never appears in setup output');
46
+ assert.equal(result.stderr.includes(token), false);
47
+ return hash(bytes);
48
+ });
49
+ assert.equal(new Set(hashes).size, 3, 'separate credentials, not a shared token');
50
+ assert.equal(existsSync(join(pilot, 'author-workspace/reviewer.token')), false);
51
+ assert.equal(existsSync(join(pilot, 'reader-workspace/reviewer.token')), false);
52
+ const retry = execute();
53
+ assert.notEqual(retry.status, 0, 'existing lab must not be reused or overwritten');
54
+ assert.deepEqual(files.map(name => hash(readToken(name))), hashes);
55
+ assert.deepEqual(fingerprint(), before, 'production harness configurations unchanged');
56
+ console.log('PASS: documented pilot setup, isolated identities, private credentials, no token output and no overwrite');
57
+ } finally {
58
+ rmSync(base, { recursive: true, force: true });
59
+ }
@@ -91,6 +91,18 @@ while [[ $# -gt 0 ]]; do
91
91
  esac
92
92
  done
93
93
 
94
+ # Refuse an overlapping purge before any harness files or registrations change.
95
+ purge_targets=()
96
+ [[ $PURGE_MEMORY -eq 1 ]] && purge_targets+=("$STORE_DIR")
97
+ [[ $PURGE_PACKS -eq 1 ]] && purge_targets+=("$PACK_DIR")
98
+ if [[ $PURGE_MEMORY -eq 1 ]]; then
99
+ for project in "${PROJECTS[@]}"; do purge_targets+=("$project/.agentfs"); done
100
+ fi
101
+ if [[ ${#purge_targets[@]} -gt 0 ]]; then
102
+ command -v node >/dev/null 2>&1 || { echo 'Uninstall purge requires Node to verify team-data retention.' >&2; exit 1; }
103
+ node "$CAIRN_ROOT/scripts/verify-team-retention.mjs" "${purge_targets[@]}" || exit 1
104
+ fi
105
+
94
106
  # ---- backup bundle -----------------------------------------------------------
95
107
  # Timestamp comes from the runtime clock on the user's machine.
96
108
  BACKUP_DIR="$HOME/.cairnkeep-uninstall-$(date +%Y%m%d-%H%M%S)"
@@ -197,6 +209,7 @@ echo " Pi live root: $PI_LIVE_ROOT"
197
209
  echo " Kimi root: $KIMI_LIVE_ROOT"
198
210
  echo " memory store: $STORE_DIR ($([[ $PURGE_MEMORY -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
199
211
  echo " context packs: $PACK_DIR ($([[ $PURGE_PACKS -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
212
+ echo " team data: retained (separate operator-managed store; no team purge option)"
200
213
  [[ ${#PROJECTS[@]} -gt 0 ]] && echo " projects: ${PROJECTS[*]}"
201
214
 
202
215
  if [[ $DRY_RUN -eq 0 && $ASSUME_YES -eq 0 ]]; then
@@ -0,0 +1,3 @@
1
+ import { assertUninstallRetainsTeam } from './lib/team-retention.mjs';
2
+ try { assertUninstallRetainsTeam(process.argv.slice(2)); }
3
+ catch (error) { console.error(error.message); process.exitCode = 1; }
@@ -27,6 +27,7 @@ import { homedir, tmpdir } from "node:os";
27
27
  import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
28
28
  import { reconcilePlaybookInstructions, removePlaybookInstructions } from "./playbook-instructions.mjs";
29
29
  import { readStableFile, readStableText, readStableJson } from "./lib/stable-file.mjs";
30
+ import { assertUninstallRetainsTeam } from "./lib/team-retention.mjs";
30
31
 
31
32
  const BOOTSTRAP_FILES = [
32
33
  ["env.example.template", ".ai/env.example"],
@@ -695,6 +696,8 @@ function uninstallWindows(root, args) {
695
696
  if (options.has("--purge-memory")) targets.push(resolve(expandHome(process.env.CAIRN_AGENTFS_BASE_DIR || join(homedir(), ".cairnkeep"))));
696
697
  if (options.has("--purge-packs")) targets.push(resolve(expandHome(process.env.CAIRN_PACK_BASE_DIR || join(homedir(), ".cairnkeep", "packs"))));
697
698
  const candidates = [...new Set(targets)].filter(existsSync);
699
+ assertUninstallRetainsTeam(candidates);
700
+ console.log('team data: retained (separate operator-managed store; no team purge option)');
698
701
  const existing = candidates.filter((target) => !candidates.some((parent) => parent !== target && target.toLowerCase().startsWith(`${parent.toLowerCase()}${sep}`)));
699
702
  if (dryRun) { for (const target of existing) console.log(`would remove: ${target}`); return; }
700
703
  const backup = join(homedir(), "cairnkeep-uninstall-backups", new Date().toISOString().replace(/[:.]/g, "-"));
@@ -770,14 +773,15 @@ export function powershellCompletion() {
770
773
  const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
771
774
  return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
772
775
  param($wordToComplete, $commandAst, $cursorPosition)
773
- $commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','security','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','proposals','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
776
+ $commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','security','team','trajectory','artifact','evidence','playbook','capabilities','mcp-tools','pack','proposals','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
774
777
  $setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
775
778
  $playbook = 'list','status','init','set','enable','disable','reset','check','record','receipts','instructions','doctor','minimal','balanced','strict','must','should','may','off','start','finish','install','remove','context.recall','context.explore','work.plan','verify.tests','review.repository','review.security','docs.update','learning.capture','--project','--json','--enforce','--complexity','--familiarity','--risk','--public-change','--changed','--change-type','--completed','--skipped','--failed','--actor','--actor-kind','--session','--policy','--decision','--event','--action','--outcome','--reason'
776
779
  $pack = 'init','lock','validate','install','import-okf','validate-okf','export-okf','list','show','remove','enable','disable','update','skills','approve-skill','revoke-skill','doctor','--repair','--project','--project-id','--json'
777
780
  $proposals = 'create','list','show','apply','doctor','--session','--scope','--model','--category','--project','--json'
778
781
  $security = 'doctor','--project','--json'
782
+ $team = 'init','project','member','credential','create','set','remove','issue','revoke','serve','mcp','list','read','search','history','propose','proposals','proposal-show','review','audit','doctor','backup','restore','memory-delete','--organization','--data','--project','--roles','--class','--expires-at','--output','--input','--confirm','--port','--key','--query','--proposal','--decision','--revision','--project-root'
779
783
  $evaluation = 'validate','run','ablate','report','prune','delete','protocol','--task-set','--adapter','--output','--repetitions','--seed','--yes','--json','--disable','--experiment','--older-than-days','--dry-run','--trajectory','--codex-jsonl','--capture-authorized'
780
- $candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+playbook(?:\\s|$)') { $playbook } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+pack(?:\\s|$)') { $pack } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+proposals(?:\\s|$)') { $proposals } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+security(?:\\s|$)') { $security } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+eval(?:\\s|$)') { $evaluation } else { $commands }
784
+ $candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+team(?:\\s|$)') { $team } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+playbook(?:\\s|$)') { $playbook } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+pack(?:\\s|$)') { $pack } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+proposals(?:\\s|$)') { $proposals } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+security(?:\\s|$)') { $security } elseif ($commandAst.ToString() -match '^\\s*cairn\\s+eval(?:\\s|$)') { $evaluation } else { $commands }
781
785
  $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
782
786
  [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
783
787
  }