@cairnkeep/cli 2.25.0 → 2.27.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 (66) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/docs/README.md +6 -0
  3. package/docs/harness-compatibility.md +5 -2
  4. package/docs/learning/CURRICULUM-MAP.md +3 -3
  5. package/docs/learning/FEATURE-GUIDE.md +13 -1
  6. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  7. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  8. package/docs/learning/lessons/L02-installation.md +2 -2
  9. package/docs/learning/lessons/L03-first-project.md +1 -1
  10. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  11. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  12. package/docs/learning/lessons/L21-context-packs.md +1 -1
  13. package/docs/learning/lessons/L22-native-windows.md +2 -2
  14. package/docs/learning/lessons/L23-guided-setup.md +1 -1
  15. package/docs/learning/lessons/L24-okf-exchange.md +1 -1
  16. package/docs/learning/lessons/L25-playbooks.md +1 -1
  17. package/docs/learning/lessons/L26-context-intelligence.md +1 -1
  18. package/docs/learning/lessons/L27-ecosystem-evidence.md +1 -1
  19. package/docs/learning/lessons/L28-security-assurance.md +5 -1
  20. package/docs/learning/lessons/L29-team-continuity.md +42 -1
  21. package/docs/learning/tracks/operator.md +5 -0
  22. package/docs/learning/video-scripts/V28-security-assurance.md +4 -0
  23. package/docs/learning/video-scripts/V29-team-continuity.md +29 -1
  24. package/docs/operating.md +10 -0
  25. package/docs/privacy-and-data-flow.md +30 -1
  26. package/docs/provider-data-flow-review.md +48 -0
  27. package/docs/releasing.md +6 -0
  28. package/docs/security-assurance.md +11 -0
  29. package/docs/storage.md +23 -0
  30. package/docs/team-onboarding.md +230 -0
  31. package/docs/team-operations.md +127 -0
  32. package/docs/team-pilot.md +2 -0
  33. package/docs/team.md +21 -4
  34. package/examples/eval/bundled-fake.json +1 -1
  35. package/mcp-memory-server/dist/capability-config.js +12 -1
  36. package/mcp-memory-server/dist/team-audit.js +41 -0
  37. package/mcp-memory-server/dist/team-cli.js +81 -30
  38. package/mcp-memory-server/dist/team-client.js +71 -0
  39. package/mcp-memory-server/dist/team-errors.js +7 -0
  40. package/mcp-memory-server/dist/team-files.js +28 -0
  41. package/mcp-memory-server/dist/team-mcp.js +12 -4
  42. package/mcp-memory-server/dist/team-pages.js +40 -0
  43. package/mcp-memory-server/dist/team-store.js +105 -40
  44. package/mcp-memory-server/package.json +2 -2
  45. package/package.json +1 -1
  46. package/scripts/cairn-cli.mjs +6 -1
  47. package/scripts/completion.sh +16 -3
  48. package/scripts/lib/team-retention.mjs +1 -1
  49. package/scripts/team-connect.mjs +366 -0
  50. package/scripts/test-ci-sharding.sh +6 -1
  51. package/scripts/test-codeql-cohort.mjs +35 -0
  52. package/scripts/test-completion.sh +7 -1
  53. package/scripts/test-package-install.sh +1 -0
  54. package/scripts/test-phase18-capability-lifecycle.sh +2 -2
  55. package/scripts/test-release-cache.mjs +16 -0
  56. package/scripts/test-security-baseline.sh +4 -0
  57. package/scripts/test-smoke-timings.mjs +19 -0
  58. package/scripts/test-team-docs.sh +26 -2
  59. package/scripts/test-team-installed.mjs +62 -0
  60. package/scripts/test-team-lab-env.mjs +9 -0
  61. package/scripts/test-team-operations.mjs +31 -0
  62. package/scripts/test-team-operations.sh +5 -0
  63. package/scripts/test-team-pilot.mjs +43 -1
  64. package/scripts/verify-published-release.mjs +11 -2
  65. package/scripts/verify-security-baseline.mjs +20 -0
  66. package/scripts/windows-platform.mjs +1 -1
@@ -0,0 +1,62 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, rmSync, existsSync, realpathSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+ import { join, resolve } from 'node:path';
7
+ import { pathToFileURL } from 'node:url';
8
+
9
+ // The caller passes the ACTUAL installed package, never a source-relative dist.
10
+ const root = resolve(process.argv[2]);
11
+ const { openTeamStore } = await import(pathToFileURL(join(root, 'mcp-memory-server/dist/team-store.js')));
12
+ const { createTeamHttpServer } = await import(pathToFileURL(join(root, 'mcp-memory-server/dist/team-http.js')));
13
+ const { hardenPrivatePath } = await import(pathToFileURL(join(root, 'mcp-memory-server/dist/platform-security.js')));
14
+ const exec = promisify(execFile);
15
+ const base = realpathSync(mkdtempSync(join(tmpdir(), 'cairn-installed-team-'))); hardenPrivatePath(base);
16
+ const project = join(base, 'project with spaces'); mkdirSync(project, { mode: 0o700 }); hardenPrivatePath(project);
17
+ const home = join(base, 'home'); mkdirSync(home, { mode: 0o700 }); hardenPrivatePath(home);
18
+ const data = join(base, 'store');
19
+ process.env.CAIRN_TEAM = '1'; process.env.CAIRN_TEAM_HTTP = '1';
20
+ let store, server;
21
+ try {
22
+ store = await openTeamStore(data, { create: true, organization: 'trial-org' });
23
+ await store.admin({ operation: 'project-create', project: 'trial-project' });
24
+ await store.admin({ operation: 'member-set', subject: 'reader', project: 'trial-project', roles: ['reader'] });
25
+ const credential = await store.issue({ subject: 'reader', credential_class: 'workload', expires_at: new Date(Date.now() + 3600000).toISOString() });
26
+ const tokenFile = join(base, 'reader.token'); writeFileSync(tokenFile, credential.token, { mode: 0o600 }); hardenPrivatePath(tokenFile);
27
+ const original = '{"mcpServers":{"cairn-memory":{"command":"personal-unchanged"},"other":{"command":"other-unchanged"}}}\n';
28
+ const config = join(project, '.mcp.json'); writeFileSync(config, original, { mode: 0o600 });
29
+ server = createTeamHttpServer(store); await new Promise(r => server.listen(0, '127.0.0.1', r));
30
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('CAIRN_')));
31
+ Object.assign(env, { HOME: home, USERPROFILE: home, CAIRN_TEAM: '1' });
32
+ const run = args => exec(process.execPath, [join(root, 'bin/cairn'), 'team', ...args], { env, cwd: base, timeout: process.platform === 'win32' ? 120000 : 30000, maxBuffer: 32768 });
33
+ const args = ['connect', '--project-root', project, '--organization', 'trial-org', '--project', 'trial-project', '--url', `http://127.0.0.1:${server.address().port}`, '--token-file', tokenFile, '--harness', 'claude'];
34
+ assert.match((await run(['connect', '--help'])).stdout, /no-write plan/);
35
+ const plan = await run(args); assert.equal(JSON.parse(plan.stdout).planned, true);
36
+ assert.equal(readFileSync(config, 'utf8'), original, 'installed no-apply preserves exact bytes');
37
+ await run([...args, '--apply', '--yes']);
38
+ const doctor = await run(['doctor', '--client', '--project-root', project]);
39
+ assert.equal(JSON.parse(doctor.stdout).ok, true);
40
+ assert.equal(doctor.stdout.includes(credential.token), false);
41
+ assert.equal(existsSync(join(home, '.cairnkeep/team')), false, 'client diagnostics open no administrator store');
42
+ await run(['disconnect', '--project-root', project]);
43
+ assert.ok(JSON.parse(readFileSync(config, 'utf8')).mcpServers['cairn-team-memory'], 'disconnect plan is read-only');
44
+ await run(['disconnect', '--project-root', project, '--apply', '--yes']);
45
+ assert.equal(readFileSync(config, 'utf8'), original, 'installed disconnect restores exact untouched original');
46
+ assert.equal(readFileSync(tokenFile, 'utf8'), credential.token);
47
+ Object.assign(env,{CAIRN_TEAM_URL:`http://127.0.0.1:${server.address().port}`,CAIRN_TEAM_ORGANIZATION:'trial-org',CAIRN_TEAM_TOKEN_FILE:tokenFile});
48
+ assert.deepEqual(JSON.parse((await run(['list','--project','trial-project','--page-size','1'])).stdout),{items:[],next_cursor:null});
49
+ await new Promise(r=>{server.close(r);server.closeAllConnections();});server=undefined;
50
+ const backup=join(base,'backup.json'),archive=join(base,'audit.json');
51
+ await run(['backup','--data',data,'--read-only','--output',backup]);
52
+ const exported=JSON.parse((await run(['audit-export','--data',data,'--backup',backup,'--output',archive])).stdout);
53
+ const archival=['audit-archive','--data',data,'--backup',backup,'--input',archive,'--confirm',exported.archive_digest];
54
+ assert.equal(JSON.parse((await run(archival)).stdout).applied,false);
55
+ assert.equal(JSON.parse((await run([...archival,'--apply'])).stdout).applied,true);
56
+ assert.equal(JSON.parse((await run(['doctor','--data',data])).stdout).capacity.checkpoint.archive_digest,exported.archive_digest);
57
+ assert(existsSync(backup) && existsSync(archive));
58
+ console.log('PASS: actual installed team plan/connect/client doctor/disconnect, preserved MCPs and no admin store');
59
+ } finally {
60
+ if (server) await new Promise(r => { server.close(r); server.closeAllConnections(); });
61
+ await store?.close(); rmSync(base, { recursive: true, force: true });
62
+ }
@@ -0,0 +1,9 @@
1
+ import assert from 'node:assert/strict';
2
+ import {teamLabEnvironment} from '../mcp-memory-server/scripts/team-lab-env.mjs';
3
+ const env=teamLabEnvironment('/private/lab',{CAIRN_TEAM:'1'},{Path:'system-path',SYSTEMROOT:'windows-root',ComSpec:'system-shell',WINDIR:'windows-dir',PATHEXT:'.EXE',PSModulePath:'system-modules',LOCALAPPDATA:'os-local',USERDOMAIN:'os-domain',CAIRN_TEAM_BASE_DIR:'production',CAIRN_LLM_API_KEY:'secret',PROVIDER_API_KEY:'secret',HOME:'real-home'});
4
+ assert.equal(env.PATH,'system-path');assert.equal(env.SystemRoot,'windows-root');assert.equal(env.ComSpec,'system-shell');assert.equal(env.WINDIR,'windows-dir');assert.equal(env.PATHEXT,'.EXE');
5
+ assert.equal(env.PSModulePath,'system-modules');assert.equal(env.LOCALAPPDATA,'os-local');assert.equal(env.USERDOMAIN,'os-domain');
6
+ for(const key of ['HOME','USERPROFILE','TEMP','TMP'])assert.equal(env[key],'/private/lab');
7
+ assert.equal(env.CAIRN_TEAM,'1');
8
+ for(const key of ['CAIRN_TEAM_BASE_DIR','CAIRN_LLM_API_KEY','PROVIDER_API_KEY'])assert.equal(env[key],undefined);
9
+ console.log('PASS: isolated team lab keeps Windows bootstrap variables, not production stores or provider secrets');
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import {teamLabEnvironment} from '../mcp-memory-server/scripts/team-lab-env.mjs';
7
+
8
+ const lab=mkdtempSync(join(tmpdir(),'cairn-team-endurance-contract-'));
9
+ try {
10
+ const script=resolve('mcp-memory-server/scripts/run-team-continuity-soak.mjs');
11
+ const out=join(lab,'report.json');
12
+ const env=teamLabEnvironment(lab);
13
+ const run=(args,enabled=false)=>spawnSync(process.execPath,[script,...args],{env:{...env,...(enabled?{CAIRN_TEAM_SOAK:'1'}:{})},encoding:'utf8',timeout:60000,maxBuffer:1024*1024});
14
+ assert.equal(run(['--help']).status,0);
15
+ assert.notEqual(run(['--out',out]).status,0,'endurance must be explicitly enabled');
16
+ assert.equal(existsSync(out),false);
17
+ const result=run(['--cycles','24','--duration-ms','0','--out',out],true);
18
+ assert.equal(result.status,0,result.stderr);
19
+ const report=JSON.parse(readFileSync(out,'utf8'));
20
+ assert.equal(report.result,'PASS');assert.equal(report.cycles,24);
21
+ assert.equal(report.agent_identities,2);assert.equal(report.api_process_restart_verified,true);
22
+ assert.equal(report.unclean_restart_verified,true);assert.equal(report.restores,1);assert.equal(report.audit_archives,1);
23
+ assert(report.credential_rotations>=2);assert(report.revocations_verified>=4);
24
+ for(const name of ['human_consent_claim','model_quality_claim','native_harness_ui_verified','production_configuration_changed'])assert.equal(report[name],false);
25
+ assert.equal(report.synthetic_only,true);assert.equal(report.paid_model_calls,0);
26
+ const bytes=readFileSync(out);
27
+ assert.notEqual(run(['--cycles','4','--duration-ms','0','--out',out],true).status,0,'reports are exclusive and cannot overwrite evidence');
28
+ assert.deepEqual(readFileSync(out),bytes);
29
+ assert.equal(existsSync(join(lab,'.cairnkeep')),false,'synthetic test must not create a real user store');
30
+ console.log('PASS: opt-in synthetic endurance, two MCP processes, API crash/restart, rotation, archive/restore and exclusive reports');
31
+ } finally {rmSync(lab,{recursive:true,force:true});}
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
4
+ cd "$ROOT"
5
+ node scripts/test-team-operations.mjs
@@ -1,10 +1,11 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { createHash } from 'node:crypto';
3
- import { spawnSync } from 'node:child_process';
3
+ import { execFile, spawnSync } from 'node:child_process';
4
4
  import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
5
5
  import { homedir, tmpdir } from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { promisify } from 'node:util';
8
9
  import { readStableFile } from './lib/stable-file.mjs';
9
10
 
10
11
  if (process.platform === 'win32') {
@@ -15,6 +16,9 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
16
  const guide = readFileSync(join(root, 'docs/team-pilot.md'), 'utf8');
16
17
  const blocks = [...guide.matchAll(/<!-- team-pilot-setup:start -->\s*```sh\n([\s\S]*?)```\s*<!-- team-pilot-setup:end -->/g)];
17
18
  assert.equal(blocks.length, 1, 'exactly one maintained executable setup block');
19
+ const onboarding = readFileSync(join(root, 'docs/team-onboarding.md'), 'utf8');
20
+ const readerBlocks = [...onboarding.matchAll(/<!-- team-reader-check:start -->\s*```sh\n([\s\S]*?)```\s*<!-- team-reader-check:end -->/g)];
21
+ assert.equal(readerBlocks.length, 1, 'exactly one maintained reader check');
18
22
  const hash = bytes => createHash('sha256').update(bytes).digest('hex');
19
23
  const productionPaths = ['.codex/config.toml', '.claude.json', '.qwen/settings.json',
20
24
  '.qwen/trustedFolders.json', '.config/opencode/opencode.json'];
@@ -52,7 +56,45 @@ try {
52
56
  const retry = execute();
53
57
  assert.notEqual(retry.status, 0, 'existing lab must not be reused or overwritten');
54
58
  assert.deepEqual(files.map(name => hash(readToken(name))), hashes);
59
+ const previousTeam = process.env.CAIRN_TEAM;
60
+ let store, server;
61
+ try {
62
+ process.env.CAIRN_TEAM = '1';
63
+ const { openTeamStore } = await import('../mcp-memory-server/dist/team-store.js');
64
+ const { createTeamHttpServer } = await import('../mcp-memory-server/dist/team-http.js');
65
+ store = await openTeamStore(join(pilot, 'store'));
66
+ const previousHttp = process.env.CAIRN_TEAM_HTTP;
67
+ try {
68
+ process.env.CAIRN_TEAM_HTTP = '1';
69
+ server = createTeamHttpServer(store);
70
+ } finally {
71
+ if (previousHttp === undefined) delete process.env.CAIRN_TEAM_HTTP;
72
+ else process.env.CAIRN_TEAM_HTTP = previousHttp;
73
+ }
74
+ await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
75
+ const readerEnvironment = { ...environment, TEAM_PILOT_URL: `http://127.0.0.1:${server.address().port}` };
76
+ // Asynchronous child execution lets the parent serve the real API request.
77
+ const runReader = (extra = '', env = readerEnvironment) => promisify(execFile)('bash', ['-c', readerBlocks[0][1] + extra], {
78
+ cwd: base, env, timeout: 30000, maxBuffer: 16384,
79
+ });
80
+ const reader = await runReader();
81
+ assert.deepEqual(JSON.parse(reader.stdout), [], 'reader lists no unapproved content');
82
+ for (const name of files) {
83
+ const token = readToken(name).toString('utf8').trim();
84
+ assert.equal((reader.stdout + reader.stderr).includes(token), false);
85
+ }
86
+ await assert.rejects(runReader('\nteam_pilot_reader proposals\n'), error => error.code === 1 && /denied/.test(error.stderr));
87
+ const { TEAM_PILOT_URL, ...missingUrl } = readerEnvironment;
88
+ await assert.rejects(runReader('', missingUrl), error => error.code !== 0 && /TEAM_PILOT_URL/.test(error.stderr));
89
+ assert.deepEqual(files.map(name => hash(readToken(name))), hashes, 'reader check does not rewrite credentials');
90
+ } finally {
91
+ if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); }
92
+ await store?.close();
93
+ if (previousTeam === undefined) delete process.env.CAIRN_TEAM;
94
+ else process.env.CAIRN_TEAM = previousTeam;
95
+ }
55
96
  assert.deepEqual(fingerprint(), before, 'production harness configurations unchanged');
97
+ console.log('PASS: documented real reader request, server role denial, required inputs and credential preservation');
56
98
  console.log('PASS: documented pilot setup, isolated identities, private credentials, no token output and no overwrite');
57
99
  } finally {
58
100
  rmSync(base, { recursive: true, force: true });
@@ -65,6 +65,14 @@ async function bytesFrom(url, maxBytes, headers = {}, blobRedirects = false) {
65
65
  const parse = bytes => JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
66
66
  const digest = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
67
67
 
68
+ export function npmVerificationCommands(prefix, version) {
69
+ const fresh = ['--cache', join(prefix, '.verification-cache'), '--prefer-online', '--prefer-offline=false', '--offline=false', '--registry', 'https://registry.npmjs.org'];
70
+ return [
71
+ ['install', '--prefix', prefix, `@cairnkeep/cli@${version}`, '--ignore-scripts', '--no-audit', '--no-fund', ...fresh],
72
+ ['audit', 'signatures', '--prefix', prefix, ...fresh],
73
+ ];
74
+ }
75
+
68
76
  async function main(args) {
69
77
  if (args.length === 1 && ["--help", "-h"].includes(args[0])) { console.log(usage); return; }
70
78
  const options = optionsFrom(args);
@@ -95,12 +103,13 @@ async function main(args) {
95
103
  tarball: async url => bytesFrom(url, 64 * 1024 * 1024),
96
104
  verifyNpm: async tarball => {
97
105
  const prefix = join(scratch, "npm");
98
- run(npm, ["install", "--prefix", prefix, `@cairnkeep/cli@${expected.version}`, "--ignore-scripts", "--no-audit", "--no-fund", "--registry", "https://registry.npmjs.org"]);
106
+ const [install, audit] = npmVerificationCommands(prefix, expected.version);
107
+ run(npm, install);
99
108
  const lock = parse(readStableFile(join(prefix, "package-lock.json"), { label: "Verification install lock", maxBytes: 8 * 1024 * 1024 }).bytes);
100
109
  if (lock.packages?.["node_modules/@cairnkeep/cli"]?.integrity !== `sha512-${createHash("sha512").update(tarball).digest("base64")}`) {
101
110
  throw new Error("Release verification failed: audited package/candidate binding.");
102
111
  }
103
- const audited = run(npm, ["audit", "signatures", "--prefix", prefix, "--registry", "https://registry.npmjs.org"]);
112
+ const audited = run(npm, audit);
104
113
  return /[1-9][0-9]* packages have verified registry signatures/.test(audited)
105
114
  && /[1-9][0-9]* packages have verified attestations/.test(audited);
106
115
  },
@@ -33,6 +33,25 @@ function walk(value, visit, path = "workflow") {
33
33
  }
34
34
  }
35
35
 
36
+ export function validateCodeqlCohort(workflows, dependabot) {
37
+ const references = [];
38
+ for (const workflow of workflows) walk(workflow, (key, value) => {
39
+ if (key === "uses" && typeof value === "string" && value.startsWith("github/codeql-action/")) {
40
+ assert.match(value, /^github\/codeql-action\/[^@\s]+@[a-f0-9]{40}$/, "CodeQL actions must be pinned to a full commit");
41
+ references.push(value);
42
+ }
43
+ });
44
+ assert.ok(references.length > 0, "CodeQL cohort is missing");
45
+ assert.equal(new Set(references.map(value => value.split("@")[1])).size, 1, "CodeQL cohort must use the same commit across workflows");
46
+ for (const action of ["init", "analyze", "upload-sarif"]) {
47
+ assert.ok(references.some(value => value.startsWith(`github/codeql-action/${action}@`)), `CodeQL cohort is missing ${action}`);
48
+ }
49
+ const updates = dependabot?.updates?.filter(update => update["package-ecosystem"] === "github-actions" && update.directory === "/") ?? [];
50
+ assert.equal(updates.length, 1, "CodeQL cohort requires one root GitHub Actions update group");
51
+ const group = updates[0].groups?.["codeql-action"];
52
+ assert.deepEqual(group, { patterns: ["github/codeql-action/*"] }, "CodeQL dependency group must cover the entire cohort without exclusions or update-type restrictions");
53
+ }
54
+
36
55
  function validateWorkflow(name, workflow) {
37
56
  assert.ok(workflow && typeof workflow === "object" && !Array.isArray(workflow), `${name} is not a workflow mapping`);
38
57
  assert.ok(workflow.permissions && typeof workflow.permissions === "object" && !Array.isArray(workflow.permissions), `${name} must have an explicit workflow permission mapping`);
@@ -123,6 +142,7 @@ for (const [index, [fixture, expected]] of maliciousFixtures.entries()) {
123
142
 
124
143
  for (const { name, workflow } of sources) validateWorkflow(name, workflow);
125
144
  validatePublicationPermissions(publish);
145
+ validateCodeqlCohort(sources.map(source => source.workflow), parse(readFileSync(join(root, ".github", "dependabot.yml"), "utf8")));
126
146
 
127
147
  const security = sources.find(({ name }) => name === "security.yml")?.text ?? "";
128
148
  assert.match(security, /github\/codeql-action\/init@[a-f0-9]{40}/);
@@ -779,7 +779,7 @@ export function powershellCompletion() {
779
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'
780
780
  $proposals = 'create','list','show','apply','doctor','--session','--scope','--model','--category','--project','--json'
781
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'
782
+ $team = 'connect','disconnect','init','project','member','credential','create','set','remove','issue','revoke','serve','mcp','list','read','search','history','propose','proposals','proposal-show','review','audit','audit-export','audit-verify','audit-archive','doctor','backup','restore','memory-delete','--backup','--previous','--page-size','--cursor','--read-only','--organization','--data','--project','--roles','--class','--expires-at','--output','--input','--confirm','--port','--key','--query','--proposal','--decision','--revision','--project-root','--connection','--client','--url','--token-file','--harness','--access','--apply','--yes'
783
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'
784
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 }
785
785
  $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {