@cairnkeep/cli 2.11.0 → 2.13.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 +33 -1
  2. package/README.md +51 -7
  3. package/docs/building-an-overlay.md +1 -1
  4. package/docs/harness-compatibility.md +40 -1
  5. package/docs/learning/CURRICULUM-MAP.md +10 -7
  6. package/docs/learning/FEATURE-GUIDE.md +14 -5
  7. package/docs/learning/README.md +4 -3
  8. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  9. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  10. package/docs/learning/lessons/L02-installation.md +16 -9
  11. package/docs/learning/lessons/L03-first-project.md +34 -15
  12. package/docs/learning/lessons/L13-session-evidence.md +48 -2
  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 +9 -9
  17. package/docs/learning/lessons/L23-guided-setup.md +6 -2
  18. package/docs/learning/tracks/evidence-and-evaluation.md +2 -1
  19. package/docs/learning/tracks/quickstart.md +5 -4
  20. package/docs/native-windows.md +23 -0
  21. package/docs/operating.md +69 -5
  22. package/docs/privacy-and-data-flow.md +44 -1
  23. package/docs/quickstart.md +91 -0
  24. package/docs/storage.md +26 -0
  25. package/docs/work-evidence.md +80 -0
  26. package/examples/eval/bundled-fake.json +1 -1
  27. package/mcp-memory-server/dist/artifact-cli.js +6 -2
  28. package/mcp-memory-server/dist/index.js +27 -0
  29. package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
  30. package/mcp-memory-server/dist/trajectory-cli.js +9 -3
  31. package/mcp-memory-server/dist/work-evidence-cli.js +121 -0
  32. package/mcp-memory-server/dist/work-evidence-schema.js +121 -0
  33. package/mcp-memory-server/dist/work-evidence-store.js +516 -0
  34. package/mcp-memory-server/package.json +2 -1
  35. package/package.json +2 -1
  36. package/schemas/cairnkeep-setup-policy.schema.json +3 -3
  37. package/schemas/cairnkeep-setup.schema.json +2 -2
  38. package/scripts/bootstrap.sh +7 -15
  39. package/scripts/cairn-cli.mjs +2 -0
  40. package/scripts/completion.sh +19 -9
  41. package/scripts/doctor.sh +35 -5
  42. package/scripts/harness-registry.mjs +126 -0
  43. package/scripts/setup-core.mjs +4 -2
  44. package/scripts/setup-reconcile.mjs +3 -2
  45. package/scripts/setup.mjs +41 -5
  46. package/scripts/test-bootstrap-untracked.sh +2 -1
  47. package/scripts/test-cli-dispatch.sh +3 -1
  48. package/scripts/test-completion.sh +6 -0
  49. package/scripts/test-harness-registry.sh +94 -0
  50. package/scripts/test-package-install.sh +9 -0
  51. package/scripts/test-phase19-eval-lifecycle.sh +12 -2
  52. package/scripts/test-setup-compatibility.mjs +5 -4
  53. package/scripts/test-uninstall.sh +10 -0
  54. package/scripts/test-windows-native.mjs +27 -1
  55. package/scripts/test-work-evidence-cli.sh +89 -0
  56. package/scripts/uninstall.sh +7 -3
  57. package/scripts/verify-docs-parity.sh +36 -0
  58. package/scripts/windows-platform.mjs +28 -7
  59. package/templates/codex-config.toml.template +5 -0
  60. package/templates/env.example.template +10 -0
  61. package/templates/start-claude.sh.template +18 -0
  62. package/templates/start-codex.sh.template +48 -0
  63. package/templates/start-kimi.sh.template +17 -0
  64. package/templates/start-opencode.sh.template +17 -0
  65. package/templates/start-pi.sh.template +17 -0
  66. package/templates/start-qwen.sh.template +17 -0
@@ -336,8 +336,18 @@ const cancelTask={...tasks[0],id:"task-cancel",input:"cancel"};
336
336
  const cancelPlan=makePlan([cancelTask]);
337
337
  const cancelStore=await reports.createEvalReportStore({root:join(fixtureRoot,"reports"),experiment_id:"cancel-fixture"});
338
338
  const controller=new AbortController();
339
- setTimeout(()=>controller.abort(),100);
340
- const cancelled=await runner.runTwoPassExperiment({plan:cancelPlan,report_store:cancelStore,temporary_root:fixtureRoot,signal:controller.signal,distill_command:{program:process.execPath,args:[distillerScript]}});
339
+ const cancellation=runner.runTwoPassExperiment({plan:cancelPlan,report_store:cancelStore,temporary_root:fixtureRoot,signal:controller.signal,distill_command:{program:process.execPath,args:[distillerScript]}});
340
+ let adapterStarted=false;
341
+ for (let attempt=0; attempt<200; attempt+=1) {
342
+ await new Promise((resolve)=>setTimeout(resolve,25));
343
+ if (existsSync(invocationLog) && readFileSync(invocationLog,"utf8").includes("task-cancel:run1")) {
344
+ adapterStarted=true;
345
+ break;
346
+ }
347
+ }
348
+ assert.equal(adapterStarted,true,"cancellation fixture adapter did not start");
349
+ controller.abort();
350
+ const cancelled=await cancellation;
341
351
  assert.equal(cancelled.report.status,"partial");
342
352
  assert.deepEqual(cancelled.report.observations.map(({terminal_state})=>terminal_state),["cancelled"]);
343
353
  assert.equal(cancelled.report.observations[0].process.cleanup!=="pending",true);
@@ -18,6 +18,7 @@ import { tmpdir } from "node:os";
18
18
  import { dirname, join, relative, resolve } from "node:path";
19
19
  import { fileURLToPath, pathToFileURL } from "node:url";
20
20
  import { bootstrapWindows } from "./windows-platform.mjs";
21
+ import { HARNESS_IDS } from "./harness-registry.mjs";
21
22
 
22
23
  const EXPECTED_RED_EXIT = 86;
23
24
  const RED_MARKER = "PHASE26_RED:SETUP_RECONCILE_MISSING";
@@ -25,8 +26,8 @@ const here = dirname(fileURLToPath(import.meta.url));
25
26
  const root = resolve(here, "..");
26
27
  const setupCorePath = join(here, "setup-core.mjs");
27
28
  const setupReconcilePath = join(here, "setup-reconcile.mjs");
28
- const HARNESSES = ["claude", "opencode", "pi", "kimi", "qwen"];
29
- const MANAGED_ROOTS = [".ai", ".planning", ".agentfs"];
29
+ const HARNESSES = HARNESS_IDS;
30
+ const MANAGED_ROOTS = [".ai", ".codex", ".planning", ".agentfs"];
30
31
 
31
32
  function run(executable, args, options = {}) {
32
33
  const result = spawnSync(executable, args, {
@@ -141,9 +142,9 @@ async function testSetupCompatibility(production, sandbox, legacy) {
141
142
  choices: Object.freeze({ git: "existing", harnesses: HARNESSES, memory: "local", confirmed: true }),
142
143
  });
143
144
  const result = await production.reconcile.reconcileSetupPlan(plan);
144
- assert.deepEqual(result.counts, { created: legacy.posixBefore.size, updated: 0, unchanged: 0, skipped: 0 });
145
+ assert.deepEqual(result.counts, { created: legacy.posixBefore.size + 1, updated: 0, unchanged: 0, skipped: 0 });
145
146
  const current = snapshot(target);
146
- assertSnapshotEqual(current, legacy.posixBefore, "deterministic setup", [".ai/cairnkeep.json"]);
147
+ assertSnapshotEqual(current, legacy.posixBefore, "deterministic setup", [".ai/cairnkeep.json", ".codex/config.toml"]);
147
148
  assert.ok(current.has(".ai/cairnkeep.json"), "setup state missing");
148
149
  const state = JSON.parse(current.get(".ai/cairnkeep.json").bytes.toString("utf8"));
149
150
  assert.deepEqual(state.harnesses, HARNESSES);
@@ -124,6 +124,7 @@ ADJACENT_PROJECT="$SB/adjacent-project"
124
124
  mkdir -p "$PROJECT_DATA/.ai" \
125
125
  "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one" \
126
126
  "$PROJECT_DATA/.agentfs/eval/experiments/partial" \
127
+ "$PROJECT_DATA/.agentfs/work-evidence/v1/records" \
127
128
  "$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent"
128
129
  printf '%s\n' '{"schema_version":1,"capabilities":{"wiki":false},"logging":{"callbacks":true}}' >"$PROJECT_DATA/.ai/capabilities.json"
129
130
  chmod 600 "$PROJECT_DATA/.ai/capabilities.json"
@@ -132,17 +133,22 @@ printf 'artifact-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/artifacts.db
132
133
  printf 'session-and-callback-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/trajectory.db"
133
134
  printf 'callback-wal-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/trajectory.db-wal"
134
135
  printf 'unrelated-memory-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/project.db"
136
+ printf 'work-evidence-v1\000durable\377bytes\n' >"$PROJECT_DATA/.agentfs/work-evidence/v1/records/wev-fixture.json"
135
137
  printf 'complete-report-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/complete/report.json"
136
138
  printf 'partial-report-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/partial/report.json"
137
139
  printf 'note-snapshot-v1\000private\377bytes\n' >"$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one/note.md"
138
140
  printf 'adjacent-eval-v1\000must-survive\377bytes\n' >"$ADJACENT_PROJECT/.agentfs/eval/experiments/adjacent/report.json"
139
141
  chmod 700 "$PROJECT_DATA/.agentfs" "$PROJECT_DATA/.agentfs/eval" \
142
+ "$PROJECT_DATA/.agentfs/work-evidence" \
143
+ "$PROJECT_DATA/.agentfs/work-evidence/v1" \
144
+ "$PROJECT_DATA/.agentfs/work-evidence/v1/records" \
140
145
  "$PROJECT_DATA/.agentfs/eval/experiments" \
141
146
  "$PROJECT_DATA/.agentfs/eval/experiments/complete" \
142
147
  "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots" \
143
148
  "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one" \
144
149
  "$PROJECT_DATA/.agentfs/eval/experiments/partial"
145
150
  chmod 600 "$PROJECT_DATA/.agentfs/project.db" \
151
+ "$PROJECT_DATA/.agentfs/work-evidence/v1/records/wev-fixture.json" \
146
152
  "$PROJECT_DATA/.agentfs/eval/experiments/complete/report.json" \
147
153
  "$PROJECT_DATA/.agentfs/eval/experiments/partial/report.json"
148
154
  chmod 400 "$PROJECT_DATA/.agentfs/eval/experiments/complete/snapshots/task-one/note.md"
@@ -155,6 +161,7 @@ cp "$PROJECT_DATA/.ai/operator-state.bin" "$SB/operator-state.before.bin"
155
161
  cp "$PROJECT_DATA/.agentfs/artifacts.db" "$SB/artifacts.before.db"
156
162
  cp "$PROJECT_DATA/.agentfs/trajectory.db" "$SB/trajectory.before.db"
157
163
  cp "$PROJECT_DATA/.agentfs/trajectory.db-wal" "$SB/trajectory.before.db-wal"
164
+ cp "$PROJECT_DATA/.agentfs/work-evidence/v1/records/wev-fixture.json" "$SB/work-evidence.before.json"
158
165
  agentfs_before=$(tree_identity "$PROJECT_DATA/.agentfs")
159
166
  eval_before=$(tree_identity "$PROJECT_DATA/.agentfs/eval")
160
167
  adjacent_before=$(tree_identity "$ADJACENT_PROJECT/.agentfs")
@@ -239,6 +246,7 @@ check "default uninstall leaves normal OpenCode installation exact" "$(diff -qr
239
246
  check "default uninstall keeps unrelated .ai bytes exact" "$(cmp -s "$SB/operator-state.before.bin" "$PROJECT_DATA/.ai/operator-state.bin" && echo yes || echo no)" "yes"
240
247
  check "default uninstall keeps callback DB exact" "$(cmp -s "$SB/trajectory.before.db" "$PROJECT_DATA/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
241
248
  check "default uninstall keeps callback WAL exact" "$(cmp -s "$SB/trajectory.before.db-wal" "$PROJECT_DATA/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
249
+ check "default uninstall keeps work evidence exact" "$(cmp -s "$SB/work-evidence.before.json" "$PROJECT_DATA/.agentfs/work-evidence/v1/records/wev-fixture.json" && echo yes || echo no)" "yes"
242
250
  check "default uninstall keeps complete and partial eval reports, snapshots, modes, and layout exact" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
243
251
  check "default uninstall keeps unrelated project memory exact" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
244
252
  check "default uninstall leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
@@ -271,6 +279,7 @@ ARTIFACT_BK=$(ls -dt "$ARTIFACT_HOME/.cairnkeep-uninstall-"* 2>/dev/null | head
271
279
  check "artifact purge backup is exact" "$(cmp -s "$SB/artifacts.before.db" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
272
280
  check "callback purge backup is exact" "$(cmp -s "$SB/trajectory.before.db" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
273
281
  check "callback WAL purge backup is exact" "$(cmp -s "$SB/trajectory.before.db-wal" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
282
+ check "work-evidence purge backup is exact" "$(cmp -s "$SB/work-evidence.before.json" "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/work-evidence/v1/records/wev-fixture.json" && echo yes || echo no)" "yes"
274
283
  check "purge backup contains exact eval tree, bytes, and modes" "$(tree_identity "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs/eval")" "$eval_before"
275
284
  check "purge backup contains exact unrelated project memory" "$(tree_identity "$ARTIFACT_BK/files/${PROJECT_DATA#/}/.agentfs")" "$agentfs_before"
276
285
  check "selected-project purge leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
@@ -278,6 +287,7 @@ HOME="$ARTIFACT_HOME" XDG_CONFIG_HOME="$ARTIFACT_HOME/.config" bash "$ARTIFACT_B
278
287
  check "artifact revert restores exact bytes" "$(cmp -s "$SB/artifacts.before.db" "$PROJECT_DATA/.agentfs/artifacts.db" && echo yes || echo no)" "yes"
279
288
  check "callback revert restores exact bytes" "$(cmp -s "$SB/trajectory.before.db" "$PROJECT_DATA/.agentfs/trajectory.db" && echo yes || echo no)" "yes"
280
289
  check "callback WAL revert restores exact bytes" "$(cmp -s "$SB/trajectory.before.db-wal" "$PROJECT_DATA/.agentfs/trajectory.db-wal" && echo yes || echo no)" "yes"
290
+ check "work-evidence revert restores exact bytes" "$(cmp -s "$SB/work-evidence.before.json" "$PROJECT_DATA/.agentfs/work-evidence/v1/records/wev-fixture.json" && echo yes || echo no)" "yes"
281
291
  check "purge revert restores exact eval tree, bytes, and modes" "$(tree_identity "$PROJECT_DATA/.agentfs/eval")" "$eval_before"
282
292
  check "purge revert restores exact unrelated project memory" "$(tree_identity "$PROJECT_DATA/.agentfs")" "$agentfs_before"
283
293
  check "purge revert leaves adjacent project exact" "$(tree_identity "$ADJACENT_PROJECT/.agentfs")" "$adjacent_before"
@@ -16,6 +16,7 @@ import {
16
16
  runWindowsCommand,
17
17
  } from "./windows-platform.mjs";
18
18
  import { runNativeContainer } from "./cairn-container-cli.mjs";
19
+ import { HARNESS_IDS } from "./harness-registry.mjs";
19
20
 
20
21
  const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
21
22
  const sandbox = mkdtempSync(join(tmpdir(), "cairn-windows-native-"));
@@ -28,13 +29,15 @@ try {
28
29
  const project = join(sandbox, "Project with spaces – Unicode");
29
30
  mkdirSync(project, { recursive: true });
30
31
  bootstrapWindows(root, [project]);
31
- for (const harness of ["claude", "opencode", "pi", "kimi", "qwen"]) {
32
+ for (const harness of HARNESS_IDS) {
32
33
  assert.ok(existsSync(join(project, ".ai", `start-${harness}.cmd`)), `${harness} cmd launcher`);
33
34
  }
34
35
  assert.match(readFileSync(join(project, ".ai", "start-claude.cmd"), "utf8"), /start-harness\.ps1/);
35
36
  assert.match(launcherModule(), /spawnSync\(command/);
36
37
  assert.match(launcherPowerShell(), /pre-launch\.ps1/);
37
38
  assert.match(launcherPowerShell(), /post-exit\.ps1/);
39
+ assert.match(launcherPowerShell(), /evidence run --harness/);
40
+ assert.match(launcherPowerShell(), /CAIRN_WORK_EVIDENCE/);
38
41
 
39
42
  const claudeRoot = join(sandbox, "Claude Config");
40
43
  await runWindowsCommand({ command: "sync", args: ["--apply", "--live-root", claudeRoot], root });
@@ -55,6 +58,7 @@ try {
55
58
  await runWindowsCommand({ command: "memory", args: ["import", archive], root });
56
59
  assert.equal(readFileSync(join(memory, "project.db"), "utf8"), "sqlite-a");
57
60
  assert.match(powershellCompletion(), /Register-ArgumentCompleter/);
61
+ assert.match(powershellCompletion(), /evidence/);
58
62
 
59
63
  let containerArgs = [];
60
64
  runNativeContainer(["stdio", "--image", "example/windows:1", "--volume", "windows-data"], root, (_engine, args) => { containerArgs = args; });
@@ -125,6 +129,28 @@ try {
125
129
  assert.match(powershellCompletion(), /--policy/);
126
130
  assert.match(powershellCompletion(), /--yes/);
127
131
  assert.match(powershellCompletion(), /--json/);
132
+ assert.match(powershellCompletion(), /codex/);
133
+
134
+ const codexProject = join(sandbox, "Guided Codex Project");
135
+ let codexOutput = "";
136
+ process.stdout.write = ((chunk) => {
137
+ codexOutput += String(chunk);
138
+ return true;
139
+ });
140
+ try {
141
+ await runWindowsCommand({
142
+ command: "setup",
143
+ args: [codexProject, "--git", "none", "--harness", "codex", "--memory", "local", "--yes", "--json"],
144
+ root,
145
+ });
146
+ } finally {
147
+ process.stdout.write = originalWrite;
148
+ }
149
+ const codexResult = JSON.parse(codexOutput);
150
+ assert.deepEqual(codexResult.harnesses, ["codex"]);
151
+ assert.equal(codexResult.machine_sync.command, null);
152
+ assert.ok(existsSync(join(codexProject, ".ai", "start-codex.cmd")));
153
+ assert.match(readFileSync(join(codexProject, ".codex", "config.toml"), "utf8"), /mcp_servers\.cairn-memory/);
128
154
  console.log("PASS: simulated Windows setup parity, private state, recovery, and completion contract");
129
155
  }
130
156
  } finally {
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
5
+ tmp=$(mktemp -d)
6
+ trap 'rm -rf "$tmp"' EXIT
7
+ fail() { echo "FAIL: $1" >&2; exit 1; }
8
+
9
+ npm --prefix "$ROOT/mcp-memory-server" run build >/dev/null
10
+
11
+ fake_bin="$tmp/bin"
12
+ mkdir -p "$fake_bin"
13
+ cat >"$fake_bin/cairn" <<EOF
14
+ #!/usr/bin/env bash
15
+ exec node "$ROOT/bin/cairn" "\$@"
16
+ EOF
17
+ chmod +x "$fake_bin/cairn"
18
+
19
+ for harness in claude opencode pi kimi qwen codex; do
20
+ project="$tmp/$harness"
21
+ mkdir -p "$project/.ai" "$project/.agentfs"
22
+ cp "$ROOT/templates/start-$harness.sh.template" "$project/.ai/start-$harness.sh"
23
+ chmod +x "$project/.ai/start-$harness.sh"
24
+ printf '*\n!.gitignore\n' >"$project/.agentfs/.gitignore"
25
+ printf 'CAIRN_WORK_EVIDENCE=1\n' >"$project/.ai/.env"
26
+ cat >"$fake_bin/$harness" <<EOF
27
+ #!/usr/bin/env bash
28
+ printf '%s\n' "\$*" > "$project/args.txt"
29
+ printf '%s\n' changed > "$project/session.txt"
30
+ exit 17
31
+ EOF
32
+ chmod +x "$fake_bin/$harness"
33
+ cat >"$project/.ai/post-exit.sh" <<EOF
34
+ printf '%s\n' "\$CAIRN_EXIT_STATUS" > "$project/post-status.txt"
35
+ EOF
36
+ (
37
+ cd "$project"
38
+ git init --quiet
39
+ git config user.email fixture@example.invalid
40
+ git config user.name Fixture
41
+ printf '%s\n' start > session.txt
42
+ git add session.txt .agentfs/.gitignore
43
+ git commit --quiet -m start
44
+ )
45
+ status=0
46
+ PATH="$fake_bin:$PATH" "$project/.ai/start-$harness.sh" --fixture "value with spaces" || status=$?
47
+ [[ $status -eq 17 ]] || fail "$harness launcher lost exit status"
48
+ grep -q -- '--fixture value with spaces' "$project/args.txt" || fail "$harness launcher lost arguments"
49
+ [[ $(cat "$project/post-status.txt") == 17 ]] || fail "$harness post-exit seam lost status"
50
+ (cd "$project" && PATH="$fake_bin:$PATH" "$ROOT/bin/cairn" evidence list --json) >"$tmp/$harness.json"
51
+ node -e '
52
+ const value=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));
53
+ if(value.evidence.length!==1||value.evidence[0].status!=="complete"||value.evidence[0].harness!==process.argv[2]||value.evidence[0].exit_status!==17)process.exit(1);
54
+ if(!value.evidence[0].touched_paths.includes("session.txt"))process.exit(1);
55
+ ' "$tmp/$harness.json" "$harness" || fail "$harness evidence record is incomplete"
56
+ done
57
+
58
+ disabled="$tmp/disabled"
59
+ mkdir -p "$disabled/.ai"
60
+ cp "$ROOT/templates/start-codex.sh.template" "$disabled/.ai/start-codex.sh"
61
+ chmod +x "$disabled/.ai/start-codex.sh"
62
+ cat >"$fake_bin/cairn" <<'EOF'
63
+ #!/usr/bin/env bash
64
+ exit 88
65
+ EOF
66
+ cat >"$fake_bin/codex" <<'EOF'
67
+ #!/usr/bin/env bash
68
+ exit 0
69
+ EOF
70
+ chmod +x "$fake_bin/cairn" "$fake_bin/codex"
71
+ PATH="$fake_bin:$PATH" "$disabled/.ai/start-codex.sh" || fail "disabled launcher called the evidence wrapper"
72
+ [[ ! -e "$disabled/.agentfs/work-evidence" ]] || fail "disabled launcher created evidence storage"
73
+
74
+ cat >"$fake_bin/codex" <<EOF
75
+ #!/usr/bin/env bash
76
+ printf launched >"$tmp/old-cairn-fallback"
77
+ exit 19
78
+ EOF
79
+ chmod +x "$fake_bin/codex"
80
+ status=0
81
+ CAIRN_WORK_EVIDENCE=1 PATH="$fake_bin:$PATH" "$disabled/.ai/start-codex.sh" 2>"$tmp/old-cairn.err" || status=$?
82
+ [[ $status -eq 19 ]] || fail "old cairn fallback lost the harness exit status"
83
+ [[ -f "$tmp/old-cairn-fallback" ]] || fail "old cairn fallback did not launch the harness"
84
+ grep -q 'compatible evidence command unavailable' "$tmp/old-cairn.err" || fail "old cairn fallback did not explain capture loss"
85
+
86
+ "$ROOT/bin/cairn" help | grep -q 'cairn evidence' || fail "root help omits evidence"
87
+ node "$ROOT/mcp-memory-server/dist/work-evidence-cli.js" --help | grep -q 'cairn evidence list' || fail "evidence help omits list"
88
+
89
+ echo "PASS: work-evidence CLI and all harness launchers preserve lifecycle semantics"
@@ -45,6 +45,7 @@ PROJECT_AI_FILES=(
45
45
  start-pi.sh
46
46
  start-kimi.sh
47
47
  start-qwen.sh
48
+ start-codex.sh
48
49
  env.example
49
50
  trajectory-redaction.json
50
51
  capabilities.json
@@ -129,9 +130,11 @@ remove_path() {
129
130
  # record. The record identifies candidates, but never grants authority to an
130
131
  # arbitrary path. Modified candidates are still backed up before removal.
131
132
  setup_owned_paths() {
132
- node --input-type=module - "$1" <<'NODE'
133
+ node --input-type=module - "$1" "$CAIRN_ROOT/scripts/harness-registry.mjs" <<'NODE'
133
134
  import { lstatSync, readFileSync } from "node:fs";
135
+ import { pathToFileURL } from "node:url";
134
136
  const path = process.argv[2];
137
+ const registry = await import(pathToFileURL(process.argv[3]).href);
135
138
  const info = lstatSync(path);
136
139
  if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) process.exit(1);
137
140
  if (process.platform !== "win32" && (info.mode & 0o777) !== 0o600) process.exit(1);
@@ -153,7 +156,8 @@ const allowed = new Set([
153
156
  ".planning/wiki/LOG.md", ".planning/alignment/policy.md", ".planning/alignment/gap-register.yaml",
154
157
  ".planning/graphs/policy.md", ".planning/graphs/.gitignore", ".planning/security/policy.md",
155
158
  ".ai/start-harness.mjs", ".ai/start-harness.ps1",
156
- ...["claude", "opencode", "pi", "kimi", "qwen"].flatMap((name) => [`.ai/start-${name}.sh`, `.ai/start-${name}.cmd`]),
159
+ ...registry.HARNESS_IDS.flatMap((name) => [`.ai/start-${name}.sh`, `.ai/start-${name}.cmd`]),
160
+ ...registry.HARNESS_IDS.flatMap((name) => registry.harnessProjectAssets(name, "local").map(({ path }) => path)),
157
161
  ]);
158
162
  for (const [asset, record] of Object.entries(state.assets)) {
159
163
  if (!allowed.has(asset)
@@ -321,7 +325,7 @@ for proj in "${PROJECTS[@]:-}"; do
321
325
  if [[ $DRY_RUN -eq 0 ]]; then
322
326
  for managed_dir in \
323
327
  "$proj/.planning/wiki" "$proj/.planning/alignment" "$proj/.planning/graphs" \
324
- "$proj/.planning/security" "$proj/.planning" "$proj/.ai" "$proj/.agentfs"; do
328
+ "$proj/.planning/security" "$proj/.planning" "$proj/.codex" "$proj/.ai" "$proj/.agentfs"; do
325
329
  rmdir "$managed_dir" 2>/dev/null || true
326
330
  done
327
331
  fi
@@ -432,6 +432,41 @@ check_artifact_remote_path_contract() {
432
432
  return "$failed"
433
433
  }
434
434
 
435
+ check_work_evidence_contract() {
436
+ local failed=0 key value source_term
437
+
438
+ while IFS='|' read -r key value source_term; do
439
+ [[ -n "$key" ]] || continue
440
+ grep -qF "$key" mcp-memory-server/src/work-evidence-schema.ts || failed=1
441
+ grep -qF "$source_term" mcp-memory-server/src/work-evidence-schema.ts || failed=1
442
+ grep -qxF "# $key=$value" templates/env.example.template || failed=1
443
+ grep -qF "$key" README.md docs/operating.md docs/work-evidence.md || failed=1
444
+ grep -qF "$value" README.md docs/operating.md docs/work-evidence.md || failed=1
445
+ done <<'EOF'
446
+ CAIRN_WORK_EVIDENCE_RETENTION_DAYS|30|WORK_EVIDENCE_DEFAULT_RETENTION_DAYS = 30
447
+ CAIRN_WORK_EVIDENCE_STORE_MAX_BYTES|67108864|WORK_EVIDENCE_DEFAULT_STORE_MAX_BYTES = 64 * 1024 * 1024
448
+ CAIRN_WORK_EVIDENCE_MAX_TOUCHED_PATHS|4096|WORK_EVIDENCE_DEFAULT_MAX_TOUCHED_PATHS = 4096
449
+ CAIRN_WORK_EVIDENCE_PATCH_MAX_BYTES|1048576|WORK_EVIDENCE_DEFAULT_PATCH_MAX_BYTES = 1024 * 1024
450
+ EOF
451
+
452
+ for key in CAIRN_WORK_EVIDENCE CAIRN_WORK_EVIDENCE_PATCH; do
453
+ grep -qF "$key" mcp-memory-server/src/work-evidence-schema.ts templates/env.example.template README.md docs/operating.md docs/privacy-and-data-flow.md || failed=1
454
+ done
455
+ for source_term in work_evidence_list work_evidence_read; do
456
+ grep -qF "\"$source_term\"" mcp-memory-server/src/index.ts || failed=1
457
+ grep -qF "$source_term" README.md docs/operating.md docs/privacy-and-data-flow.md docs/work-evidence.md || failed=1
458
+ done
459
+ for source_term in 'list [--status pending|complete] [--json]' 'show <evidence-id-or-prefix> [--json]' 'delete <evidence-id-or-prefix> [--dry-run] [--json]' 'prune [--dry-run] [--json]' 'doctor [--repair] [--json]'; do
460
+ grep -qF "$source_term" mcp-memory-server/src/work-evidence-cli.ts || failed=1
461
+ done
462
+ grep -qF '.agentfs/work-evidence/v1/' docs/storage.md docs/privacy-and-data-flow.md docs/work-evidence.md || failed=1
463
+ grep -qF 'cairn evidence' docs/learning/CURRICULUM-MAP.md docs/learning/FEATURE-GUIDE.md docs/learning/lessons/L13-session-evidence.md || failed=1
464
+ grep -qF 'never exposed by the HTTP transport' docs/work-evidence.md || failed=1
465
+
466
+ [[ "$failed" -eq 0 ]] && echo "[work-evidence-contract] OK: flags/defaults/tools/storage/privacy/learning match source"
467
+ return "$failed"
468
+ }
469
+
435
470
  check_capability_contract() {
436
471
  local failed=0 file id env tool term source_file docs_files
437
472
 
@@ -624,6 +659,7 @@ main() {
624
659
  check_typed_contract || failed=1
625
660
  check_artifact_contract || failed=1
626
661
  check_artifact_remote_path_contract || failed=1
662
+ check_work_evidence_contract || failed=1
627
663
  check_capability_contract || failed=1
628
664
  check_native_capability_docs || failed=1
629
665
 
@@ -20,6 +20,7 @@ import { gzipSync, gunzipSync } from "node:zlib";
20
20
  import { spawnSync } from "node:child_process";
21
21
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
22
22
  import { homedir, tmpdir } from "node:os";
23
+ import { HARNESS_IDS, harnessProjectAssets } from "./harness-registry.mjs";
23
24
 
24
25
  const BOOTSTRAP_FILES = [
25
26
  ["env.example.template", ".ai/env.example"],
@@ -39,7 +40,7 @@ const BOOTSTRAP_FILES = [
39
40
  ["security-policy.md.template", ".planning/security/policy.md"],
40
41
  ];
41
42
 
42
- const HARNESSES = ["claude", "opencode", "pi", "kimi", "qwen"];
43
+ const HARNESSES = HARNESS_IDS;
43
44
  const HOOK_EVENTS = new Map([
44
45
  ["memory-wakeup", ["SessionStart", ""]],
45
46
  ["memory-capture", ["SessionEnd", ""]],
@@ -189,7 +190,19 @@ if ($env:CAIRN_EXTRA_SETTINGS -and $Harness -eq 'claude') {
189
190
  if ($env:CAIRN_EXTRA_SETTINGS -and $Harness -eq 'opencode') { $env:OPENCODE_CONFIG = $env:CAIRN_EXTRA_SETTINGS }
190
191
  Push-Location $ProjectRoot
191
192
  try {
192
- & $Harness @HarnessArgs
193
+ $EvidenceEnabled = $env:CAIRN_WORK_EVIDENCE -match '^(1|true|yes|on)$'
194
+ $CairnCommand = if ($EvidenceEnabled) { Get-Command cairn -ErrorAction SilentlyContinue } else { $null }
195
+ $EvidenceSupported = $false
196
+ if ($EvidenceEnabled -and $CairnCommand) {
197
+ & $CairnCommand.Source evidence --help *> $null
198
+ $EvidenceSupported = ($LASTEXITCODE -eq 0)
199
+ }
200
+ if ($EvidenceSupported) {
201
+ & $CairnCommand.Source evidence run --harness $Harness -- $Harness @HarnessArgs
202
+ } else {
203
+ if ($EvidenceEnabled) { Write-Warning 'a compatible cairn evidence command is unavailable; launching without capture' }
204
+ & $Harness @HarnessArgs
205
+ }
193
206
  $Status = if ($null -eq $LASTEXITCODE) { 0 } else { $LASTEXITCODE }
194
207
  } finally {
195
208
  Pop-Location
@@ -249,7 +262,8 @@ export function bootstrapWindows(root, args) {
249
262
  installFile(join(templates, source), join(target, ...destination.split("/")), destination.endsWith("capabilities.json") ? 0o600 : 0o644);
250
263
  }
251
264
  for (const harness of HARNESSES) {
252
- installFile(join(templates, `start-${harness}.sh.template`), join(target, ".ai", `start-${harness}.sh`), 0o755);
265
+ const asset = harnessProjectAssets(harness, "local")[0];
266
+ installFile(join(templates, asset.template), join(target, ...asset.path.split("/")), asset.mode);
253
267
  const cmdPath = join(target, ".ai", `start-${harness}.cmd`);
254
268
  if (!existsSync(cmdPath)) atomicWrite(cmdPath, windowsLauncher(harness), 0o755);
255
269
  }
@@ -267,7 +281,9 @@ export function bootstrapWindows(root, args) {
267
281
  }
268
282
 
269
283
  console.log(`\nCairnkeep bootstrapped into ${target}`);
270
- console.log(`Launch from PowerShell or Command Prompt with ${join(target, ".ai", "start-claude.cmd")}`);
284
+ console.log(`Codex: run 'cairn setup' in the project and select Codex, review trust, then use ${join(target, ".ai", "start-codex.cmd")}`);
285
+ console.log(`Claude: register 'cairn memory-server', run 'cairn sync --apply', then use ${join(target, ".ai", "start-claude.cmd")}`);
286
+ console.log("For new projects, prefer 'cairn setup'; guide: docs/quickstart.md");
271
287
  }
272
288
 
273
289
  function walkFiles(root, predicate = () => true, prefix = "") {
@@ -436,6 +452,7 @@ async function doctorWindows(root, args) {
436
452
  for (const [label, file, doctorArgs] of [
437
453
  ["trajectory", "trajectory-cli.js", ["doctor", "--json", ...(repair ? ["--repair"] : [])]],
438
454
  ["artifact", "artifact-cli.js", ["doctor", "--json", ...(repair ? ["--repair"] : [])]],
455
+ ["work evidence", "work-evidence-cli.js", ["doctor", "--json", ...(repair ? ["--repair"] : [])]],
439
456
  ["typed memory", "node-cli.js", ["doctor", "--project-root", process.cwd(), ...(repair ? ["--repair"] : [])]],
440
457
  ["capability", "capability-cli.js", ["doctor", "--json"]],
441
458
  ["context pack", "context-pack-cli.js", ["doctor", "--json"]],
@@ -616,7 +633,10 @@ function uninstallWindows(root, args) {
616
633
  for (const project of projects) {
617
634
  const base = resolve(project);
618
635
  for (const [, rel] of BOOTSTRAP_FILES) targets.push(join(base, ...rel.split("/")));
619
- for (const harness of HARNESSES) targets.push(join(base, ".ai", `start-${harness}.cmd`));
636
+ for (const harness of HARNESSES) {
637
+ for (const asset of harnessProjectAssets(harness, "local")) targets.push(join(base, ...asset.path.split("/")));
638
+ targets.push(join(base, ".ai", `start-${harness}.cmd`));
639
+ }
620
640
  targets.push(join(base, ".ai", "start-harness.mjs"));
621
641
  targets.push(join(base, ".ai", "start-harness.ps1"));
622
642
  if (options.has("--purge-memory")) targets.push(join(base, ".agentfs"));
@@ -685,10 +705,11 @@ foreach ($Item in $Manifest.items) {
685
705
  }
686
706
 
687
707
  export function powershellCompletion() {
708
+ const harnesses = HARNESS_IDS.map((id) => `'${id}'`).join(",");
688
709
  return `Register-ArgumentCompleter -Native -CommandName cairn -ScriptBlock {
689
710
  param($wordToComplete, $commandAst, $cursorPosition)
690
- $commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','trajectory','artifact','capabilities','mcp-tools','pack','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
691
- $setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none','claude','opencode','pi','kimi','qwen','local'
711
+ $commands = 'bootstrap','setup','memory-server','sync','sync-pi','sync-kimi','doctor','trajectory','artifact','evidence','capabilities','mcp-tools','pack','notes','eval','skill','graph','memory','audit-timer','uninstall','completion','version','help'
712
+ $setup = '--git','--harness','--memory','--policy','--yes','--json','init','existing','none',${harnesses},'local'
692
713
  $candidates = if ($commandAst.ToString() -match '^\\s*cairn\\s+setup(?:\\s|$)') { $setup } else { $commands }
693
714
  $candidates | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
694
715
  [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
@@ -0,0 +1,5 @@
1
+ # Cairnkeep project-scoped memory server. Codex loads project configuration only
2
+ # after the project has been trusted.
3
+ [mcp_servers.cairn-memory]
4
+ command = "cairn"
5
+ args = ["memory-server"]
@@ -65,6 +65,16 @@
65
65
  # Bootstrap creates .ai/trajectory-redaction.json for optional custom patterns.
66
66
  # CAIRN_TRAJECTORY_REDACTION_FILE=.ai/trajectory-redaction.json
67
67
  #
68
+ # Git-linked work evidence is local and OFF by default. Generated harness
69
+ # launchers capture bounded Git state before and after a session. Optional patch
70
+ # capture also requires the artifact store; no patch is ever applied by Cairnkeep.
71
+ # CAIRN_WORK_EVIDENCE=1
72
+ # CAIRN_WORK_EVIDENCE_PATCH=1
73
+ # CAIRN_WORK_EVIDENCE_RETENTION_DAYS=30
74
+ # CAIRN_WORK_EVIDENCE_STORE_MAX_BYTES=67108864
75
+ # CAIRN_WORK_EVIDENCE_MAX_TOUCHED_PATHS=4096
76
+ # CAIRN_WORK_EVIDENCE_PATCH_MAX_BYTES=1048576
77
+ #
68
78
  # Harness-produced compaction continuity and explicit artifact access are local
69
79
  # and OFF by default. Either capability may be enabled independently; neither
70
80
  # needs a model, API key, or network endpoint. HTTP artifact tools require the
@@ -68,6 +68,24 @@ if [[ -n "${CAIRN_EXTRA_SETTINGS:-}" ]]; then
68
68
  launch_args+=(--settings "$CAIRN_EXTRA_SETTINGS")
69
69
  fi
70
70
 
71
+ case "${CAIRN_WORK_EVIDENCE:-}" in
72
+ 1|true|TRUE|yes|YES|on|ON)
73
+ if command -v cairn >/dev/null 2>&1 && cairn evidence --help >/dev/null 2>&1; then
74
+ set +e
75
+ cairn evidence run --harness claude -- claude "${launch_args[@]}" "$@"
76
+ CAIRN_EXIT_STATUS=$?
77
+ set -e
78
+ export CAIRN_EXIT_STATUS
79
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
80
+ # shellcheck disable=SC1091
81
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
82
+ fi
83
+ exit "$CAIRN_EXIT_STATUS"
84
+ fi
85
+ echo "cairn: compatible evidence command unavailable; launching without capture" >&2
86
+ ;;
87
+ esac
88
+
71
89
  if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
72
90
  # Run (not exec) so the post-exit hook fires after the harness returns.
73
91
  set +e
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Cairnkeep launcher for Codex CLI. Loads this project's private .ai/.env and
3
+ # starts Codex in the project root. The project-scoped MCP registration lives in
4
+ # .codex/config.toml and is only loaded after Codex trusts the project.
5
+ set -euo pipefail
6
+ ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
7
+
8
+ if [[ -f "$ROOT_DIR/.ai/.env" ]]; then
9
+ set -a
10
+ # shellcheck disable=SC1091
11
+ . "$ROOT_DIR/.ai/.env"
12
+ set +a
13
+ fi
14
+ if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
15
+ # shellcheck disable=SC1091
16
+ . "$ROOT_DIR/.ai/pre-launch.sh"
17
+ fi
18
+ cd "$ROOT_DIR"
19
+
20
+ case "${CAIRN_WORK_EVIDENCE:-}" in
21
+ 1|true|TRUE|yes|YES|on|ON)
22
+ if command -v cairn >/dev/null 2>&1 && cairn evidence --help >/dev/null 2>&1; then
23
+ set +e
24
+ cairn evidence run --harness codex -- codex "$@"
25
+ CAIRN_EXIT_STATUS=$?
26
+ set -e
27
+ export CAIRN_EXIT_STATUS
28
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
29
+ # shellcheck disable=SC1091
30
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
31
+ fi
32
+ exit "$CAIRN_EXIT_STATUS"
33
+ fi
34
+ echo "cairn: compatible evidence command unavailable; launching without capture" >&2
35
+ ;;
36
+ esac
37
+
38
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
39
+ set +e
40
+ codex "$@"
41
+ CAIRN_EXIT_STATUS=$?
42
+ set -e
43
+ export CAIRN_EXIT_STATUS
44
+ # shellcheck disable=SC1091
45
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
46
+ exit "$CAIRN_EXIT_STATUS"
47
+ fi
48
+ exec codex "$@"
@@ -26,6 +26,23 @@ fi
26
26
 
27
27
  cd "$ROOT_DIR"
28
28
 
29
+ case "${CAIRN_WORK_EVIDENCE:-}" in
30
+ 1|true|TRUE|yes|YES|on|ON)
31
+ if command -v cairn >/dev/null 2>&1 && cairn evidence --help >/dev/null 2>&1; then
32
+ set +e
33
+ cairn evidence run --harness kimi -- kimi "$@"
34
+ CAIRN_EXIT_STATUS=$?
35
+ set -e
36
+ export CAIRN_EXIT_STATUS
37
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
38
+ # shellcheck disable=SC1091
39
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
40
+ fi
41
+ exit "$CAIRN_EXIT_STATUS"
42
+ fi
43
+ echo "cairn: compatible evidence command unavailable; launching without capture" >&2
44
+ ;;
45
+ esac
29
46
  if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
30
47
  set +e
31
48
  kimi "$@"
@@ -70,6 +70,23 @@ if [[ -n "${CAIRN_EXTRA_SETTINGS:-}" ]]; then
70
70
  export OPENCODE_CONFIG="$CAIRN_EXTRA_SETTINGS"
71
71
  fi
72
72
 
73
+ case "${CAIRN_WORK_EVIDENCE:-}" in
74
+ 1|true|TRUE|yes|YES|on|ON)
75
+ if command -v cairn >/dev/null 2>&1 && cairn evidence --help >/dev/null 2>&1; then
76
+ set +e
77
+ cairn evidence run --harness opencode -- opencode "$@"
78
+ CAIRN_EXIT_STATUS=$?
79
+ set -e
80
+ export CAIRN_EXIT_STATUS
81
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
82
+ # shellcheck disable=SC1091
83
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
84
+ fi
85
+ exit "$CAIRN_EXIT_STATUS"
86
+ fi
87
+ echo "cairn: compatible evidence command unavailable; launching without capture" >&2
88
+ ;;
89
+ esac
73
90
  if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
74
91
  set +e
75
92
  opencode "$@"
@@ -17,6 +17,23 @@ if [[ -f "$ROOT_DIR/.ai/pre-launch.sh" ]]; then
17
17
  fi
18
18
  cd "$ROOT_DIR"
19
19
 
20
+ case "${CAIRN_WORK_EVIDENCE:-}" in
21
+ 1|true|TRUE|yes|YES|on|ON)
22
+ if command -v cairn >/dev/null 2>&1 && cairn evidence --help >/dev/null 2>&1; then
23
+ set +e
24
+ cairn evidence run --harness pi -- pi "$@"
25
+ CAIRN_EXIT_STATUS=$?
26
+ set -e
27
+ export CAIRN_EXIT_STATUS
28
+ if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
29
+ # shellcheck disable=SC1091
30
+ . "$ROOT_DIR/.ai/post-exit.sh" || true
31
+ fi
32
+ exit "$CAIRN_EXIT_STATUS"
33
+ fi
34
+ echo "cairn: compatible evidence command unavailable; launching without capture" >&2
35
+ ;;
36
+ esac
20
37
  if [[ -f "$ROOT_DIR/.ai/post-exit.sh" ]]; then
21
38
  set +e
22
39
  pi "$@"