@cairnkeep/cli 2.15.4 → 2.17.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.
- package/CHANGELOG.md +44 -0
- package/CONTRIBUTING.md +12 -0
- package/README.md +85 -590
- package/docs/README.md +87 -0
- package/docs/agents.md +193 -0
- package/docs/context-intelligence.md +143 -0
- package/docs/context-packs.md +33 -0
- package/docs/domain-knowledge.md +29 -0
- package/docs/ecosystem.md +23 -0
- package/docs/learning/CURRICULUM-MAP.md +3 -1
- package/docs/learning/FEATURE-GUIDE.md +23 -4
- package/docs/learning/README.md +4 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +3 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +9 -10
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +2 -2
- package/docs/learning/lessons/L23-guided-setup.md +18 -5
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/learning/lessons/L26-context-intelligence.md +180 -0
- package/docs/learning/tracks/evidence-and-evaluation.md +1 -0
- package/docs/learning/tracks/operator.md +1 -0
- package/docs/learning/tracks/practitioner.md +1 -0
- package/docs/learning/video-scripts/V26-context-intelligence.md +144 -0
- package/docs/native-windows.md +5 -0
- package/docs/operating.md +65 -3
- package/docs/privacy-and-data-flow.md +22 -0
- package/docs/quickstart.md +12 -4
- package/docs/releasing.md +27 -19
- package/docs/storage.md +21 -0
- package/docs/work-evidence.md +14 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/context-pack-cli.js +3 -1
- package/mcp-memory-server/dist/context-pack-retrieval.js +413 -0
- package/mcp-memory-server/dist/context-pack.js +171 -3
- package/mcp-memory-server/dist/domain-retrieval-provider.js +257 -0
- package/mcp-memory-server/dist/index.js +73 -387
- package/mcp-memory-server/dist/mcp-tool-catalog.js +2 -0
- package/mcp-memory-server/dist/memory-extraction.js +87 -0
- package/mcp-memory-server/dist/memory-proposal-cli.js +110 -0
- package/mcp-memory-server/dist/memory-proposal-schema.js +78 -0
- package/mcp-memory-server/dist/memory-proposal-store.js +273 -0
- package/mcp-memory-server/dist/platform-security.js +32 -26
- package/mcp-memory-server/dist/retrieval-benchmark.js +199 -0
- package/mcp-memory-server/dist/reviewed-memory-store.js +281 -0
- package/mcp-memory-server/dist/work-evidence-schema.js +17 -0
- package/mcp-memory-server/dist/work-evidence-store.js +57 -1
- package/mcp-memory-server/package.json +8 -1
- package/package.json +1 -1
- package/schemas/context-usage-receipt.schema.json +41 -0
- package/schemas/memory-proposal.schema.json +65 -0
- package/schemas/retrieval-benchmark.schema.json +64 -0
- package/scripts/cairn-cli.mjs +3 -0
- package/scripts/completion.sh +17 -6
- package/scripts/doctor.sh +4 -2
- package/scripts/setup.mjs +92 -13
- package/scripts/terminal-prompts.mjs +172 -0
- package/scripts/test-completion.sh +8 -1
- package/scripts/test-docs-navigation.sh +63 -0
- package/scripts/test-doctor.sh +21 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-release-sbom.sh +44 -16
- package/scripts/test-setup-preflight.mjs +130 -0
- package/scripts/test-windows-native.mjs +30 -0
- package/scripts/verify-docs-parity.sh +28 -28
- package/scripts/windows-platform.mjs +8 -5
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/context-usage-receipt.schema.json",
|
|
4
|
+
"title": "Cairnkeep context usage receipt v1",
|
|
5
|
+
"description": "A privacy-minimal link from existing work evidence to a retrieved result. Receipts contain identifiers, digests, outcome labels and a timestamp; never retrieved content, queries, prompts or model output.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"link_id",
|
|
11
|
+
"evidence_id",
|
|
12
|
+
"kind",
|
|
13
|
+
"created_at",
|
|
14
|
+
"receipt_id",
|
|
15
|
+
"task_digest",
|
|
16
|
+
"result_digest",
|
|
17
|
+
"outcome"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"schema_version": { "const": 1 },
|
|
21
|
+
"link_id": {
|
|
22
|
+
"description": "Equal to receipt_id for context usage links.",
|
|
23
|
+
"type": "string",
|
|
24
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
25
|
+
},
|
|
26
|
+
"evidence_id": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"pattern": "^wev_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
29
|
+
},
|
|
30
|
+
"kind": { "const": "context_usage" },
|
|
31
|
+
"created_at": { "type": "string", "format": "date-time" },
|
|
32
|
+
"receipt_id": {
|
|
33
|
+
"description": "SHA-256 of canonical evidence_id, task_digest and result_digest fields. The outcome is deliberately excluded so a changed outcome is a conflict, not a second receipt.",
|
|
34
|
+
"type": "string",
|
|
35
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
36
|
+
},
|
|
37
|
+
"task_digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
38
|
+
"result_digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
39
|
+
"outcome": { "enum": ["used", "unused", "unknown"] }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/memory-proposal.schema.json",
|
|
4
|
+
"title": "Cairnkeep review-gated memory proposal",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "created_at", "project_root", "scope", "source", "extraction", "candidates", "digest"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"created_at": { "type": "string", "format": "date-time" },
|
|
11
|
+
"project_root": { "type": "string", "minLength": 1 },
|
|
12
|
+
"scope": { "type": "string", "pattern": "^(project|[a-z0-9][a-z0-9-]*)$" },
|
|
13
|
+
"source": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["kind", "session_id", "digest"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"kind": { "const": "trajectory" },
|
|
19
|
+
"session_id": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
20
|
+
"digest": { "$ref": "#/$defs/digest" }
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"extraction": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["model"],
|
|
27
|
+
"properties": { "model": { "type": "string", "minLength": 1, "maxLength": 256 } }
|
|
28
|
+
},
|
|
29
|
+
"candidates": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"minItems": 1,
|
|
32
|
+
"maxItems": 128,
|
|
33
|
+
"items": { "$ref": "#/$defs/candidate" }
|
|
34
|
+
},
|
|
35
|
+
"digest": { "$ref": "#/$defs/digest" }
|
|
36
|
+
},
|
|
37
|
+
"$defs": {
|
|
38
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
39
|
+
"candidate": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["key", "value", "operation", "base_hash", "value_hash"],
|
|
43
|
+
"$comment": "Runtime verification also requires value_hash to equal SHA-256(value), noop base_hash to equal value_hash, and update base_hash to differ from value_hash.",
|
|
44
|
+
"properties": {
|
|
45
|
+
"key": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
46
|
+
"value": { "type": "string", "maxLength": 65536 },
|
|
47
|
+
"category": { "type": "string", "maxLength": 64 },
|
|
48
|
+
"importance": { "type": "number", "minimum": 0, "maximum": 1 },
|
|
49
|
+
"operation": { "enum": ["create", "update", "noop"] },
|
|
50
|
+
"base_hash": { "oneOf": [{ "$ref": "#/$defs/digest" }, { "type": "null" }] },
|
|
51
|
+
"value_hash": { "$ref": "#/$defs/digest" }
|
|
52
|
+
},
|
|
53
|
+
"allOf": [
|
|
54
|
+
{
|
|
55
|
+
"if": { "properties": { "operation": { "const": "create" } }, "required": ["operation"] },
|
|
56
|
+
"then": { "properties": { "base_hash": { "type": "null" } } }
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"if": { "properties": { "operation": { "enum": ["update", "noop"] } }, "required": ["operation"] },
|
|
60
|
+
"then": { "properties": { "base_hash": { "$ref": "#/$defs/digest" } } }
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://cairnkeep.dev/schemas/retrieval-benchmark.schema.json",
|
|
4
|
+
"title": "Cairnkeep retrieval benchmark suite",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "id", "description", "packs", "cases"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
11
|
+
"description": { "type": "string", "minLength": 1 },
|
|
12
|
+
"packs": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"minItems": 1,
|
|
15
|
+
"maxItems": 32,
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["id", "version", "title", "enabled", "files"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
22
|
+
"version": { "type": "string", "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" },
|
|
23
|
+
"title": { "type": "string", "minLength": 1 },
|
|
24
|
+
"enabled": { "type": "boolean" },
|
|
25
|
+
"files": {
|
|
26
|
+
"type": "array",
|
|
27
|
+
"minItems": 1,
|
|
28
|
+
"maxItems": 1024,
|
|
29
|
+
"items": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"additionalProperties": false,
|
|
32
|
+
"required": ["path", "kind", "title", "description", "keywords", "content"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"path": { "type": "string", "minLength": 1, "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$" },
|
|
35
|
+
"kind": { "enum": ["document", "skill"] },
|
|
36
|
+
"title": { "type": "string", "minLength": 1 },
|
|
37
|
+
"description": { "type": "string" },
|
|
38
|
+
"keywords": { "type": "array", "items": { "type": "string" } },
|
|
39
|
+
"content": { "type": "string" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"cases": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"minItems": 1,
|
|
49
|
+
"maxItems": 1024,
|
|
50
|
+
"items": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"required": ["id", "query", "relevant", "forbidden", "limit"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"id": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
56
|
+
"query": { "type": "string", "minLength": 1 },
|
|
57
|
+
"relevant": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
|
|
58
|
+
"forbidden": { "type": "array", "uniqueItems": true, "items": { "type": "string", "minLength": 1 } },
|
|
59
|
+
"limit": { "type": "integer", "minimum": 1, "maximum": 100 }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
package/scripts/cairn-cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
9
9
|
const NODE_COMMANDS = new Map([
|
|
10
10
|
["memory-server", "index.js"],
|
|
11
11
|
["trajectory", "trajectory-cli.js"],
|
|
12
|
+
["proposals", "memory-proposal-cli.js"],
|
|
12
13
|
["artifact", "artifact-cli.js"],
|
|
13
14
|
["capabilities", "capability-cli.js"],
|
|
14
15
|
["mcp-tools", "mcp-tool-cli.js"],
|
|
@@ -37,6 +38,7 @@ export const USAGE = `cairn — Cairnkeep CLI
|
|
|
37
38
|
|
|
38
39
|
Usage:
|
|
39
40
|
cairn bootstrap [--untracked] [path]
|
|
41
|
+
cairn setup [PATH] # interactive Git, harness, memory, and confirmation selectors
|
|
40
42
|
cairn setup PATH --git init|existing|none --harness LIST --memory local|none [--policy PATH] --yes [--json]
|
|
41
43
|
cairn memory-server
|
|
42
44
|
cairn sync [--check|--apply] [--live-root DIR]
|
|
@@ -44,6 +46,7 @@ Usage:
|
|
|
44
46
|
cairn sync-kimi [--check|--apply] [--live-root DIR]
|
|
45
47
|
cairn doctor [--repair]
|
|
46
48
|
cairn trajectory <list|show|prune>
|
|
49
|
+
cairn proposals <create|list|show|apply|doctor>
|
|
47
50
|
cairn artifact <list|show|delete|prune>
|
|
48
51
|
cairn capabilities <list|status|enable|disable|reset|logging>
|
|
49
52
|
cairn mcp-tools <list|status|set|reset>
|
package/scripts/completion.sh
CHANGED
|
@@ -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 doctor trajectory artifact evidence playbook capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help"
|
|
16
|
+
commands="bootstrap setup memory-server sync sync-pi sync-kimi doctor 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
|
|
@@ -57,7 +57,8 @@ _cairn_complete() {
|
|
|
57
57
|
esac
|
|
58
58
|
;;
|
|
59
59
|
mcp-tools) COMPREPLY=( $(compgen -W "list status set reset full read-only custom --tool --project --json" -- "$current") ) ;;
|
|
60
|
-
pack) COMPREPLY=( $(compgen -W "init lock validate install import-okf validate-okf export-okf list show remove enable disable update skills approve-skill revoke-skill --id --version --title --description --license --ref --project --project-id --output --file --note --check --apply --confirm --json" -- "$current") ) ;;
|
|
60
|
+
pack) COMPREPLY=( $(compgen -W "init lock validate install import-okf validate-okf export-okf list show remove enable disable update skills approve-skill revoke-skill doctor --id --version --title --description --license --ref --project --project-id --output --file --note --check --apply --confirm --repair --json" -- "$current") ) ;;
|
|
61
|
+
proposals) COMPREPLY=( $(compgen -W "create list show apply doctor --session --scope --model --category --project --json" -- "$current") ) ;;
|
|
61
62
|
notes) COMPREPLY=( $(compgen -W "distill search-error promote doctor --project --session --all-projects --para-root --text --component --with --confirm --repair --json" -- "$current") ) ;;
|
|
62
63
|
eval)
|
|
63
64
|
case "${COMP_WORDS[2]:-}" in
|
|
@@ -101,6 +102,7 @@ _cairn() {
|
|
|
101
102
|
'capabilities:inspect and manage project capability state'
|
|
102
103
|
'mcp-tools:inspect and restrict MCP tool exposure'
|
|
103
104
|
'pack:manage immutable context packs and skill approvals'
|
|
105
|
+
'proposals:review and apply session memory proposals'
|
|
104
106
|
'notes:distill and search local hindsight notes'
|
|
105
107
|
'eval:run and inspect default-off local evaluations'
|
|
106
108
|
'skill:review and evaluate evidence-backed skill improvements'
|
|
@@ -136,7 +138,8 @@ _cairn() {
|
|
|
136
138
|
evidence) _values 'work evidence command' list show delete prune doctor '--status[filter records]:status:(pending complete)' '--dry-run[report without mutation]' '--repair[remove safe temporary remnants]' '--json[emit JSON]' ;;
|
|
137
139
|
playbook) _values 'playbook command' 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[project root]:directory:_files -/' '--json[emit JSON]' '--enforce[fail when must evidence is missing]' '--changed[changed project path]:file:_files' '--change-type[change type]:type:(code tests docs config dependencies security)' '--complexity[task complexity]:level:(trivial standard complex)' '--familiarity[context familiarity]:level:(known mixed unfamiliar)' '--risk[task risk]:level:(low normal high security)' '--public-change[public behavior changed]' '--completed[completed action]:action:' '--skipped[skipped action and reason]:evidence:' '--failed[failed action and reason]:evidence:' '--actor[local actor label]:actor:' '--actor-kind[local actor kind]:kind:(user agent service)' '--session[session label]:session:' '--policy[policy digest]:digest:' '--decision[decision digest]:digest:' '--event[lifecycle event]:event:(start check finish)' '--action[canonical action]:action:(context.recall context.explore work.plan verify.tests review.repository review.security docs.update learning.capture)' '--outcome[outcome]:outcome:(completed skipped failed)' '--reason[bounded reason]:reason:' ;;
|
|
138
140
|
mcp-tools) _values 'MCP tool profile command' list status set reset full read-only custom '--tool[allow an exact tool]:tool:' '--project[project root]:directory:_files -/' '--json[emit JSON]' ;;
|
|
139
|
-
pack) _values 'context pack command' init lock validate install import-okf validate-okf export-okf list show remove enable disable update skills approve-skill revoke-skill '--ref[pinned Git ref]:ref:' '--project[project root]:directory:_files -/' '--project-id[remote project ID]:project ID:' '--output[export output directory]:directory:_files -/' '--file[explicit project Markdown file]:file:_files' '--note[promoted shared note ID]:note ID:' '--check[inspect update or export]' '--apply[apply update or export]' '--confirm[confirm digest]:digest:' '--json[emit JSON]' ;;
|
|
141
|
+
pack) _values 'context pack command' init lock validate install import-okf validate-okf export-okf list show remove enable disable update skills approve-skill revoke-skill doctor '--ref[pinned Git ref]:ref:' '--project[project root]:directory:_files -/' '--project-id[remote project ID]:project ID:' '--output[export output directory]:directory:_files -/' '--file[explicit project Markdown file]:file:_files' '--note[promoted shared note ID]:note ID:' '--check[inspect update or export]' '--apply[apply update or export]' '--confirm[confirm digest]:digest:' '--repair[remove only invalid derived context cache state]' '--json[emit JSON]' ;;
|
|
142
|
+
proposals) _values 'memory proposal command' create list show apply doctor '--session[trajectory session ID]:session:' '--scope[memory scope]:scope:' '--model[extraction model]:model:' '--category[extraction category]:category:' '--project[project root]:directory:_files -/' '--json[emit JSON]' ;;
|
|
140
143
|
capabilities)
|
|
141
144
|
case $words[3] in
|
|
142
145
|
list|status) _arguments '--json[emit JSON]' ;;
|
|
@@ -185,8 +188,8 @@ EOF
|
|
|
185
188
|
fish)
|
|
186
189
|
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
187
190
|
complete -c cairn -f
|
|
188
|
-
for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help
|
|
189
|
-
|
|
191
|
+
for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
|
|
192
|
+
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
190
193
|
end
|
|
191
194
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
|
|
192
195
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
|
|
@@ -243,12 +246,20 @@ complete -c cairn -n "__fish_seen_subcommand_from mcp-tools; and __fish_seen_sub
|
|
|
243
246
|
complete -c cairn -n "__fish_seen_subcommand_from mcp-tools" -l project -r
|
|
244
247
|
complete -c cairn -n "__fish_seen_subcommand_from mcp-tools" -l tool -r
|
|
245
248
|
complete -c cairn -n "__fish_seen_subcommand_from mcp-tools" -l json
|
|
246
|
-
complete -c cairn -n "__fish_seen_subcommand_from pack" -a "init lock validate install import-okf validate-okf export-okf list show remove enable disable update skills approve-skill revoke-skill"
|
|
249
|
+
complete -c cairn -n "__fish_seen_subcommand_from pack" -a "init lock validate install import-okf validate-okf export-okf list show remove enable disable update skills approve-skill revoke-skill doctor"
|
|
247
250
|
complete -c cairn -n "__fish_seen_subcommand_from pack" -l project -r
|
|
248
251
|
complete -c cairn -n "__fish_seen_subcommand_from pack" -l project-id -r
|
|
249
252
|
complete -c cairn -n "__fish_seen_subcommand_from pack" -l ref -r
|
|
250
253
|
complete -c cairn -n "__fish_seen_subcommand_from pack" -l confirm -r
|
|
251
254
|
complete -c cairn -n "__fish_seen_subcommand_from pack" -l json
|
|
255
|
+
complete -c cairn -n "__fish_seen_subcommand_from pack; and __fish_seen_subcommand_from doctor" -l repair
|
|
256
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -a "create list show apply doctor"
|
|
257
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l session -r
|
|
258
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l scope -r
|
|
259
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l model -r
|
|
260
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l category -r
|
|
261
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l project -r
|
|
262
|
+
complete -c cairn -n "__fish_seen_subcommand_from proposals" -l json
|
|
252
263
|
complete -c cairn -n "__fish_seen_subcommand_from notes" -a "distill search-error promote doctor"
|
|
253
264
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and not __fish_seen_subcommand_from validate run ablate report prune delete" -a "validate run ablate report prune delete"
|
|
254
265
|
complete -c cairn -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from validate run ablate" -l task-set -r
|
package/scripts/doctor.sh
CHANGED
|
@@ -13,7 +13,7 @@ REPAIR_STORES=0
|
|
|
13
13
|
while [[ $# -gt 0 ]]; do
|
|
14
14
|
case "$1" in
|
|
15
15
|
--repair) REPAIR_STORES=1; shift ;;
|
|
16
|
-
-h|--help) echo "Usage: cairn doctor [--repair] # repair derived
|
|
16
|
+
-h|--help) echo "Usage: cairn doctor [--repair] # repair safe derived pack caches, typed metadata/indexes, and interrupted note transactions"; exit 0 ;;
|
|
17
17
|
*) echo "Unknown doctor option: $1" >&2; echo "Usage: cairn doctor [--repair]" >&2; exit 2 ;;
|
|
18
18
|
esac
|
|
19
19
|
done
|
|
@@ -470,11 +470,13 @@ fi
|
|
|
470
470
|
pack_cli="$CAIRN_ROOT/mcp-memory-server/dist/context-pack-cli.js"
|
|
471
471
|
pack_base="${CAIRN_PACK_BASE_DIR:-$HOME/.cairnkeep/packs}"
|
|
472
472
|
pack_base="${pack_base/#\~/$HOME}"
|
|
473
|
+
pack_args=(doctor --json)
|
|
474
|
+
if [[ "$REPAIR_STORES" == 1 ]]; then pack_args+=(--repair); fi
|
|
473
475
|
if [[ ! -e "$pack_base" ]]; then
|
|
474
476
|
skip "context packs (not installed)"
|
|
475
477
|
elif [[ ! -f "$pack_cli" ]]; then
|
|
476
478
|
fail "context-pack checker is missing from the installed package"
|
|
477
|
-
elif pack_json=$(node "$pack_cli"
|
|
479
|
+
elif pack_json=$(node "$pack_cli" "${pack_args[@]}" 2>/dev/null) && node -e 'const v=JSON.parse(process.argv[1]); if(v.ok!==true)process.exit(1)' "$pack_json"; then
|
|
478
480
|
pack_counts=$(node -e 'const v=JSON.parse(process.argv[1]); process.stdout.write(`${v.objects} object(s), ${v.projects} project pointer(s)`)' "$pack_json")
|
|
479
481
|
pass "context packs healthy ($pack_counts)"
|
|
480
482
|
else
|
package/scripts/setup.mjs
CHANGED
|
@@ -21,8 +21,9 @@ import {
|
|
|
21
21
|
resolveSetupChoices,
|
|
22
22
|
} from "./setup-core.mjs";
|
|
23
23
|
import { reconcileSetupPlan } from "./setup-reconcile.mjs";
|
|
24
|
-
import { HARNESS_IDS, machineSyncCommand, requiredHarnessAssetPaths } from "./harness-registry.mjs";
|
|
24
|
+
import { HARNESS_IDS, HARNESS_REGISTRY, machineSyncCommand, requiredHarnessAssetPaths } from "./harness-registry.mjs";
|
|
25
25
|
import { reconcilePlaybookInstructions } from "./playbook-instructions.mjs";
|
|
26
|
+
import { selectManyPrompt, selectOnePrompt, supportsTerminalPrompts } from "./terminal-prompts.mjs";
|
|
26
27
|
|
|
27
28
|
const HARNESSES = HARNESS_IDS;
|
|
28
29
|
const GIT_MODES = Object.freeze(["init", "existing", "none"]);
|
|
@@ -70,11 +71,36 @@ function parseInteractiveHarnesses(value) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export async function promptSetupChoices(parsed, streams) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
let linePrompt = null;
|
|
75
|
+
let externallyClosed = false;
|
|
76
|
+
const question = typeof streams.question === "function"
|
|
77
|
+
? streams.question
|
|
78
|
+
: async (message) => {
|
|
79
|
+
linePrompt ??= createInterface({ input: streams.input, output: streams.output });
|
|
80
|
+
return linePrompt.question(message);
|
|
81
|
+
};
|
|
82
|
+
const closeLinePrompt = () => {
|
|
83
|
+
if (linePrompt) linePrompt.close();
|
|
84
|
+
linePrompt = null;
|
|
85
|
+
if (!externallyClosed) {
|
|
86
|
+
streams.closePrompt?.();
|
|
87
|
+
externallyClosed = true;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const structured = Boolean(streams.selectOne && streams.selectMany)
|
|
91
|
+
|| supportsTerminalPrompts(streams.input, streams.output);
|
|
92
|
+
const selectOne = streams.selectOne ?? ((options) => selectOnePrompt({
|
|
93
|
+
...options,
|
|
94
|
+
input: streams.input,
|
|
95
|
+
output: streams.output,
|
|
96
|
+
}));
|
|
97
|
+
const selectMany = streams.selectMany ?? ((options) => selectManyPrompt({
|
|
98
|
+
...options,
|
|
99
|
+
input: streams.input,
|
|
100
|
+
output: streams.output,
|
|
101
|
+
}));
|
|
76
102
|
try {
|
|
77
|
-
const target = parsed.target ?? (await
|
|
103
|
+
const target = parsed.target ?? (await question("Target path: ")).trim();
|
|
78
104
|
if (!target) throw operational("interactive-input", "A setup target path is required.");
|
|
79
105
|
const preflight = classifySetupTarget(target);
|
|
80
106
|
const gitQuestion = preflight.targetState === "missing" || preflight.targetState === "empty"
|
|
@@ -82,12 +108,63 @@ export async function promptSetupChoices(parsed, streams) {
|
|
|
82
108
|
: preflight.repository === "work-tree"
|
|
83
109
|
? "Git mode (existing recommended; choices: init, existing, none): "
|
|
84
110
|
: "Git mode (init requires explicit choice for existing non-Git target; choices: init, existing, none): ";
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
if (structured) closeLinePrompt();
|
|
112
|
+
const gitChoices = preflight.gitExecutable === "missing"
|
|
113
|
+
? [{ value: "none", label: "Continue without Git", hint: "Git executable not found; repository features will be limited" }]
|
|
114
|
+
: preflight.repository === "work-tree"
|
|
115
|
+
? [
|
|
116
|
+
{ value: "existing", label: "Use the existing Git repository", hint: "recommended" },
|
|
117
|
+
{ value: "none", label: "Continue without Git integration", hint: "limited" },
|
|
118
|
+
]
|
|
119
|
+
: [
|
|
120
|
+
{ value: "init", label: "Initialize a Git repository", hint: preflight.targetState === "non-empty" ? "review the existing directory first" : "recommended" },
|
|
121
|
+
{ value: "none", label: "Continue without Git integration", hint: "limited" },
|
|
122
|
+
];
|
|
123
|
+
const git = parsed.git ?? (structured
|
|
124
|
+
? await selectOne({ message: "Git integration", choices: gitChoices, initialValue: gitChoices[0].value })
|
|
125
|
+
: (await question(gitQuestion)).trim());
|
|
126
|
+
const harnesses = parsed.harnesses ?? (structured
|
|
127
|
+
? await selectMany({
|
|
128
|
+
message: "Coding harnesses",
|
|
129
|
+
choices: HARNESS_REGISTRY.map(({ id, title }) => ({ value: id, label: `${title} (${id})` })),
|
|
130
|
+
})
|
|
131
|
+
: parseInteractiveHarnesses(await question(`Harnesses (${HARNESSES.join(", ")}): `)));
|
|
132
|
+
const memory = parsed.memory ?? (structured
|
|
133
|
+
? await selectOne({
|
|
134
|
+
message: "Memory mode",
|
|
135
|
+
choices: [
|
|
136
|
+
{ value: "local", label: "Local Cairnkeep memory", hint: "recommended" },
|
|
137
|
+
{ value: "none", label: "No memory server" },
|
|
138
|
+
],
|
|
139
|
+
initialValue: "local",
|
|
140
|
+
})
|
|
141
|
+
: (await question("Memory mode (local, none): ")).trim());
|
|
142
|
+
return {
|
|
143
|
+
target,
|
|
144
|
+
git,
|
|
145
|
+
harnesses,
|
|
146
|
+
memory,
|
|
147
|
+
preflight,
|
|
148
|
+
terminal: {
|
|
149
|
+
question,
|
|
150
|
+
close: closeLinePrompt,
|
|
151
|
+
confirm: structured
|
|
152
|
+
? async () => (await selectOne({
|
|
153
|
+
message: "Apply this setup plan?",
|
|
154
|
+
choices: [
|
|
155
|
+
{ value: "no", label: "No, cancel" },
|
|
156
|
+
{ value: "yes", label: "Yes, apply the plan" },
|
|
157
|
+
],
|
|
158
|
+
initialValue: "no",
|
|
159
|
+
})) === "yes"
|
|
160
|
+
: async () => {
|
|
161
|
+
const answer = (await question("Apply this setup plan? [y/N] ")).trim().toLowerCase();
|
|
162
|
+
return answer === "y" || answer === "yes";
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
};
|
|
89
166
|
} catch (error) {
|
|
90
|
-
|
|
167
|
+
closeLinePrompt();
|
|
91
168
|
throw error;
|
|
92
169
|
}
|
|
93
170
|
}
|
|
@@ -167,7 +244,7 @@ function initializeGit(target) {
|
|
|
167
244
|
}
|
|
168
245
|
|
|
169
246
|
async function executeSetup(args, options) {
|
|
170
|
-
const isTTY = options.isTTY ?? Boolean(options.input.isTTY);
|
|
247
|
+
const isTTY = options.isTTY ?? Boolean(options.input.isTTY && options.output.isTTY);
|
|
171
248
|
const parsed = parseSetupArgs(args, { isTTY });
|
|
172
249
|
const policy = parsed.policyPath ? readSetupPolicy(parsed.policyPath) : null;
|
|
173
250
|
|
|
@@ -196,9 +273,9 @@ async function executeSetup(args, options) {
|
|
|
196
273
|
|
|
197
274
|
if (interactive && !parsed.confirmed) {
|
|
198
275
|
renderPlan(plan, options.output);
|
|
199
|
-
const
|
|
276
|
+
const accepted = await interactive.terminal.confirm();
|
|
200
277
|
interactive.terminal.close();
|
|
201
|
-
if (
|
|
278
|
+
if (!accepted) {
|
|
202
279
|
writeLine(options.output, "Setup cancelled; no files were changed.");
|
|
203
280
|
return null;
|
|
204
281
|
}
|
|
@@ -221,6 +298,8 @@ export async function runSetup(args, options = {}) {
|
|
|
221
298
|
augmentPlan: options.augmentPlan,
|
|
222
299
|
question: options.question,
|
|
223
300
|
closePrompt: options.closePrompt,
|
|
301
|
+
selectOne: options.selectOne,
|
|
302
|
+
selectMany: options.selectMany,
|
|
224
303
|
};
|
|
225
304
|
try {
|
|
226
305
|
const result = await executeSetup(args, streams);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { emitKeypressEvents } from "node:readline";
|
|
2
|
+
|
|
3
|
+
function promptError(message, status = 1) {
|
|
4
|
+
const error = new Error(message);
|
|
5
|
+
error.name = "TerminalPromptError";
|
|
6
|
+
error.status = status;
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizedChoices(choices) {
|
|
11
|
+
if (!Array.isArray(choices) || choices.length === 0) throw promptError("A terminal prompt requires at least one choice.");
|
|
12
|
+
const normalized = choices.map((choice) => Object.freeze({
|
|
13
|
+
value: String(choice.value),
|
|
14
|
+
label: String(choice.label),
|
|
15
|
+
hint: choice.hint ? String(choice.hint) : "",
|
|
16
|
+
}));
|
|
17
|
+
if (new Set(normalized.map(({ value }) => value)).size !== normalized.length) {
|
|
18
|
+
throw promptError("Terminal prompt choices must have unique values.");
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze(normalized);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function supportsTerminalPrompts(input, output, env = process.env) {
|
|
24
|
+
return Boolean(
|
|
25
|
+
input?.isTTY
|
|
26
|
+
&& output?.isTTY
|
|
27
|
+
&& typeof input.setRawMode === "function"
|
|
28
|
+
&& env.TERM !== "dumb",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function runKeypressPrompt({ input, output, draw, accept }) {
|
|
33
|
+
emitKeypressEvents(input);
|
|
34
|
+
const wasRaw = Boolean(input.isRaw);
|
|
35
|
+
const wasPaused = typeof input.isPaused === "function" ? input.isPaused() : false;
|
|
36
|
+
let renderedLines = 0;
|
|
37
|
+
let settled = false;
|
|
38
|
+
let keypressHandler = null;
|
|
39
|
+
let rawChanged = false;
|
|
40
|
+
let resumed = false;
|
|
41
|
+
let cursorHidden = false;
|
|
42
|
+
|
|
43
|
+
const render = () => {
|
|
44
|
+
const lines = draw();
|
|
45
|
+
if (renderedLines > 0) output.write(`\x1b[${renderedLines}A\r\x1b[0J`);
|
|
46
|
+
output.write(`${lines.join("\n")}\n`);
|
|
47
|
+
renderedLines = lines.length;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const clear = () => {
|
|
51
|
+
if (renderedLines > 0) output.write(`\x1b[${renderedLines}A\r\x1b[0J`);
|
|
52
|
+
renderedLines = 0;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
if (!wasRaw) {
|
|
57
|
+
input.setRawMode(true);
|
|
58
|
+
rawChanged = true;
|
|
59
|
+
}
|
|
60
|
+
if (wasPaused && typeof input.resume === "function") {
|
|
61
|
+
input.resume();
|
|
62
|
+
resumed = true;
|
|
63
|
+
}
|
|
64
|
+
output.write("\x1b[?25l");
|
|
65
|
+
cursorHidden = true;
|
|
66
|
+
render();
|
|
67
|
+
return await new Promise((resolve, reject) => {
|
|
68
|
+
const finish = (callback, value) => {
|
|
69
|
+
if (settled) return;
|
|
70
|
+
settled = true;
|
|
71
|
+
input.removeListener("keypress", onKeypress);
|
|
72
|
+
callback(value);
|
|
73
|
+
};
|
|
74
|
+
const onKeypress = (text, key = {}) => {
|
|
75
|
+
try {
|
|
76
|
+
if ((key.ctrl && key.name === "c") || key.name === "escape") {
|
|
77
|
+
finish(reject, promptError("Setup cancelled.", 130));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const result = accept(text, key);
|
|
81
|
+
if (result?.done) finish(resolve, result.value);
|
|
82
|
+
else render();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
finish(reject, error);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
keypressHandler = onKeypress;
|
|
88
|
+
input.on("keypress", onKeypress);
|
|
89
|
+
});
|
|
90
|
+
} finally {
|
|
91
|
+
if (keypressHandler) input.removeListener("keypress", keypressHandler);
|
|
92
|
+
clear();
|
|
93
|
+
if (cursorHidden) output.write("\x1b[?25h");
|
|
94
|
+
if (rawChanged) input.setRawMode(false);
|
|
95
|
+
if (resumed && typeof input.pause === "function") input.pause();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function movement(key, cursor, length) {
|
|
100
|
+
if (key.name === "up" || key.name === "k") return (cursor - 1 + length) % length;
|
|
101
|
+
if (key.name === "down" || key.name === "j") return (cursor + 1) % length;
|
|
102
|
+
if (key.name === "home") return 0;
|
|
103
|
+
if (key.name === "end") return length - 1;
|
|
104
|
+
return cursor;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function selectOnePrompt({ message, choices, initialValue, input, output }) {
|
|
108
|
+
const options = normalizedChoices(choices);
|
|
109
|
+
let cursor = Math.max(0, options.findIndex(({ value }) => value === initialValue));
|
|
110
|
+
const selected = await runKeypressPrompt({
|
|
111
|
+
input,
|
|
112
|
+
output,
|
|
113
|
+
draw: () => [
|
|
114
|
+
`? ${message} (Up/Down to move, Enter to select)`,
|
|
115
|
+
...options.map((choice, index) => `${index === cursor ? ">" : " "} ${choice.label}${choice.hint ? ` - ${choice.hint}` : ""}`),
|
|
116
|
+
],
|
|
117
|
+
accept: (text, key) => {
|
|
118
|
+
cursor = movement(key, cursor, options.length);
|
|
119
|
+
if (key.name === "return" || key.name === "enter") return { done: true, value: options[cursor].value };
|
|
120
|
+
const number = Number.parseInt(text, 10);
|
|
121
|
+
if (Number.isInteger(number) && number >= 1 && number <= options.length) {
|
|
122
|
+
cursor = number - 1;
|
|
123
|
+
return { done: true, value: options[cursor].value };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const label = options.find(({ value }) => value === selected)?.label ?? selected;
|
|
129
|
+
output.write(`> ${message}: ${label}\n`);
|
|
130
|
+
return selected;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function selectManyPrompt({ message, choices, selectedValues = [], input, output, minimum = 1 }) {
|
|
134
|
+
const options = normalizedChoices(choices);
|
|
135
|
+
const allowed = new Set(options.map(({ value }) => value));
|
|
136
|
+
const selected = new Set(selectedValues.filter((value) => allowed.has(value)));
|
|
137
|
+
let cursor = 0;
|
|
138
|
+
let validation = "";
|
|
139
|
+
const values = await runKeypressPrompt({
|
|
140
|
+
input,
|
|
141
|
+
output,
|
|
142
|
+
draw: () => [
|
|
143
|
+
`? ${message} (Up/Down to move, Space to toggle, A to toggle all, Enter to continue)`,
|
|
144
|
+
...options.map((choice, index) => `${index === cursor ? ">" : " "} [${selected.has(choice.value) ? "x" : " "}] ${choice.label}${choice.hint ? ` - ${choice.hint}` : ""}`),
|
|
145
|
+
validation,
|
|
146
|
+
],
|
|
147
|
+
accept: (text, key) => {
|
|
148
|
+
cursor = movement(key, cursor, options.length);
|
|
149
|
+
if (key.name === "space" || text === " ") {
|
|
150
|
+
const value = options[cursor].value;
|
|
151
|
+
if (selected.has(value)) selected.delete(value);
|
|
152
|
+
else selected.add(value);
|
|
153
|
+
validation = "";
|
|
154
|
+
} else if (text?.toLowerCase() === "a") {
|
|
155
|
+
if (selected.size === options.length) selected.clear();
|
|
156
|
+
else for (const { value } of options) selected.add(value);
|
|
157
|
+
validation = "";
|
|
158
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
159
|
+
if (selected.size < minimum) {
|
|
160
|
+
validation = ` Select at least ${minimum} option${minimum === 1 ? "" : "s"}.`;
|
|
161
|
+
output.write("\x07");
|
|
162
|
+
} else {
|
|
163
|
+
return { done: true, value: options.filter(({ value }) => selected.has(value)).map(({ value }) => value) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const labels = options.filter(({ value }) => values.includes(value)).map(({ label }) => label);
|
|
170
|
+
output.write(`> ${message}: ${labels.join(", ")}\n`);
|
|
171
|
+
return values;
|
|
172
|
+
}
|
|
@@ -46,7 +46,13 @@ for shell in bash zsh fish; do
|
|
|
46
46
|
grep -q 'mcp-tools' "$tmp/$shell"
|
|
47
47
|
grep -q 'full.*read-only.*custom\|full read-only custom' "$tmp/$shell"
|
|
48
48
|
grep -q 'pack' "$tmp/$shell"
|
|
49
|
-
grep -q 'init.*lock.*validate.*install.*list.*show.*remove.*enable.*disable.*update.*skills.*approve-skill.*revoke-skill\|init lock validate install list show remove enable disable update skills approve-skill revoke-skill' "$tmp/$shell"
|
|
49
|
+
grep -q 'init.*lock.*validate.*install.*list.*show.*remove.*enable.*disable.*update.*skills.*approve-skill.*revoke-skill.*doctor\|init lock validate install list show remove enable disable update skills approve-skill revoke-skill doctor' "$tmp/$shell"
|
|
50
|
+
grep -q -- '--repair\|-l repair' "$tmp/$shell"
|
|
51
|
+
grep -q 'proposals' "$tmp/$shell"
|
|
52
|
+
grep -q 'create.*list.*show.*apply.*doctor\|create list show apply doctor' "$tmp/$shell"
|
|
53
|
+
for proposal_flag in --session --scope --model --category --project --json; do
|
|
54
|
+
grep -q -- "$proposal_flag\|-l ${proposal_flag#--}" "$tmp/$shell"
|
|
55
|
+
done
|
|
50
56
|
grep -q -- '--purge-packs\|-l purge-packs' "$tmp/$shell"
|
|
51
57
|
grep -q 'evidence' "$tmp/$shell"
|
|
52
58
|
grep -q 'list.*show.*delete.*prune.*doctor\|list show delete prune doctor' "$tmp/$shell"
|
|
@@ -77,6 +83,7 @@ fi
|
|
|
77
83
|
grep -qF 'cairn skill <harvest|list|show|review|propose|evaluate|apply|rollback>' "$tmp/root-help"
|
|
78
84
|
grep -qF 'cairn evidence <list|show|delete|prune|doctor>' "$tmp/root-help"
|
|
79
85
|
grep -qF 'cairn playbook <list|status|init|set|enable|disable|reset|check|record|receipts|instructions|doctor>' "$tmp/root-help"
|
|
86
|
+
grep -qF 'cairn proposals <create|list|show|apply|doctor>' "$tmp/root-help"
|
|
80
87
|
node "$ROOT/mcp-memory-server/dist/skill-cli.js" --help >"$tmp/skill-help"
|
|
81
88
|
for command in $skill_commands; do
|
|
82
89
|
grep -q "cairn skill $command" "$tmp/skill-help"
|