lingo-loop 0.1.0__py3-none-any.whl
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.
- language_tutor/__init__.py +3 -0
- language_tutor/_assets/.claude-plugin/plugin.json +9 -0
- language_tutor/_assets/.codex-plugin/plugin.json +12 -0
- language_tutor/_assets/hermes-profile/SOUL.md +30 -0
- language_tutor/_assets/hermes-profile/config.yaml +37 -0
- language_tutor/_assets/hermes-profile/distribution.yaml +49 -0
- language_tutor/_assets/openclaw-plugin/openclaw.plugin.json +47 -0
- language_tutor/_assets/openclaw-plugin/package.json +39 -0
- language_tutor/_assets/openclaw-plugin/src/index.ts +68 -0
- language_tutor/_assets/openclaw-plugin/tsconfig.json +17 -0
- language_tutor/adapters/__init__.py +1 -0
- language_tutor/adapters/base.py +178 -0
- language_tutor/adapters/claude.py +21 -0
- language_tutor/adapters/codex.py +16 -0
- language_tutor/adapters/hermes.py +16 -0
- language_tutor/adapters/openclaw.py +16 -0
- language_tutor/adapters/registry.py +81 -0
- language_tutor/boot_context.py +114 -0
- language_tutor/cli.py +1039 -0
- language_tutor/dal/__init__.py +1 -0
- language_tutor/dal/migrations.py +77 -0
- language_tutor/dal/paths.py +48 -0
- language_tutor/dal/repositories.py +1117 -0
- language_tutor/dal/sqlite_store.py +29 -0
- language_tutor/dal/yaml_store.py +54 -0
- language_tutor/errors.py +101 -0
- language_tutor/evaluators.py +25 -0
- language_tutor/feedback.py +207 -0
- language_tutor/health.py +70 -0
- language_tutor/installer/__init__.py +35 -0
- language_tutor/installer/assets.py +100 -0
- language_tutor/installer/protocol.py +52 -0
- language_tutor/installer/providers/__init__.py +1 -0
- language_tutor/installer/providers/base.py +326 -0
- language_tutor/installer/providers/claude.py +19 -0
- language_tutor/installer/providers/codex.py +19 -0
- language_tutor/installer/providers/hermes.py +19 -0
- language_tutor/installer/providers/openclaw.py +24 -0
- language_tutor/installer/registry.py +44 -0
- language_tutor/installer/seams.py +124 -0
- language_tutor/installer/service.py +70 -0
- language_tutor/lessons.py +47 -0
- language_tutor/lifecycle.py +91 -0
- language_tutor/progress.py +426 -0
- language_tutor/progress_rendering.py +137 -0
- language_tutor/reading.py +66 -0
- language_tutor/schemas.py +1442 -0
- language_tutor/setup.py +43 -0
- language_tutor/srs.py +46 -0
- language_tutor/text_modalities.py +288 -0
- language_tutor/vocab.py +505 -0
- language_tutor/writing.py +35 -0
- lingo_loop-0.1.0.dist-info/METADATA +130 -0
- lingo_loop-0.1.0.dist-info/RECORD +57 -0
- lingo_loop-0.1.0.dist-info/WHEEL +4 -0
- lingo_loop-0.1.0.dist-info/entry_points.txt +2 -0
- lingo_loop-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"name": "language-tutor",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Local-first language tutor packaged as a Codex plugin (reuses root skills/).",
|
|
6
|
+
"author": "language-tutor contributors",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"skills": "./skills/",
|
|
9
|
+
"features": {
|
|
10
|
+
"plugin_hooks": false
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Language Tutor (Hermes Profile Prompt)
|
|
2
|
+
|
|
3
|
+
You are a patient, source-backed language tutor running inside the Hermes
|
|
4
|
+
host. You operate text-only: reading, lessons, transcripts, vocabulary,
|
|
5
|
+
writing, and progress reporting. You do not produce or consume audio or
|
|
6
|
+
images.
|
|
7
|
+
|
|
8
|
+
## Lifecycle
|
|
9
|
+
|
|
10
|
+
Hermes does not provide SessionStart/SessionEnd hooks for this profile.
|
|
11
|
+
Boot context is built on demand via an explicit tutor command (the
|
|
12
|
+
operator runs the tutor's boot command after `hermes profile install`).
|
|
13
|
+
There is no automatic session-end hook; end-of-session work is invoked
|
|
14
|
+
explicitly when requested.
|
|
15
|
+
|
|
16
|
+
## Boot context
|
|
17
|
+
|
|
18
|
+
When the explicit tutor boot command runs, load the learner profile and
|
|
19
|
+
preferences through the tutor core and render the boot context sections.
|
|
20
|
+
Never invent learner state; rely on the core's persisted data, which lives
|
|
21
|
+
outside this distribution.
|
|
22
|
+
|
|
23
|
+
## Behavior contract
|
|
24
|
+
|
|
25
|
+
- Keep all interaction text-only and terminal-readable.
|
|
26
|
+
- Defer pedagogy, feedback, progress, and persistence to the tutor core.
|
|
27
|
+
- Never echo, package, or export learner secrets, memories, sessions,
|
|
28
|
+
database state, logs, or local overrides.
|
|
29
|
+
- Stay within the declared capability profile: no audio, no images, no
|
|
30
|
+
host-specific side effects beyond the documented text flows.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Hermes profile configuration defaults for the language-tutor agent.
|
|
2
|
+
# Defaults only. No learner data, no secrets, no machine-local state.
|
|
3
|
+
schema_version: 1
|
|
4
|
+
|
|
5
|
+
# Capability declaration mirrors src/language_tutor/adapters/registry.py
|
|
6
|
+
# (HostId.HERMES). Source of truth stays in the registry; this is a
|
|
7
|
+
# documentation echo for operators inspecting the distribution.
|
|
8
|
+
capabilities:
|
|
9
|
+
text_support: supported
|
|
10
|
+
audio_support: unsupported
|
|
11
|
+
image_support: unsupported
|
|
12
|
+
lifecycle_start: explicit_command
|
|
13
|
+
lifecycle_end: not_available
|
|
14
|
+
boot_context_trigger: explicit_tutor_command
|
|
15
|
+
|
|
16
|
+
# Tutor flows exposed by this profile. All text-only.
|
|
17
|
+
flows:
|
|
18
|
+
- reading
|
|
19
|
+
- lesson
|
|
20
|
+
- transcript
|
|
21
|
+
- vocab
|
|
22
|
+
- writing
|
|
23
|
+
- progress
|
|
24
|
+
|
|
25
|
+
# Hermes profile lifecycle commands (documentation for operators).
|
|
26
|
+
commands:
|
|
27
|
+
install: hermes profile install
|
|
28
|
+
update: hermes profile update
|
|
29
|
+
inspect: hermes profile info language-tutor
|
|
30
|
+
list: hermes profile list
|
|
31
|
+
remove: hermes profile delete language-tutor
|
|
32
|
+
|
|
33
|
+
# Operator-supplied environment is read from the operator's own untracked
|
|
34
|
+
# .env at runtime. This distribution ships no values.
|
|
35
|
+
env:
|
|
36
|
+
source: operator_local_env
|
|
37
|
+
bundled_values: false
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Hermes profile distribution manifest for the language-tutor agent.
|
|
2
|
+
# Distribution metadata and defaults only. Contains NO learner-owned data.
|
|
3
|
+
# Installed/updated via `hermes profile install` / `hermes profile update`.
|
|
4
|
+
schema_version: 1
|
|
5
|
+
name: language-tutor
|
|
6
|
+
display_name: Language Tutor
|
|
7
|
+
version: 0.1.0
|
|
8
|
+
description: >-
|
|
9
|
+
Source-backed language-learning tutor profile for Hermes. Provides
|
|
10
|
+
text-only reading, lesson, transcript, vocab, writing, and progress
|
|
11
|
+
flows backed by the host-independent tutor core. No audio or image
|
|
12
|
+
capabilities.
|
|
13
|
+
license: MIT
|
|
14
|
+
source_url: https://github.com/artemVeduta/lingo-loop
|
|
15
|
+
|
|
16
|
+
# Whole-agent package contents (git-backed distribution).
|
|
17
|
+
profile:
|
|
18
|
+
prompt: SOUL.md
|
|
19
|
+
config: config.yaml
|
|
20
|
+
# Tutor skills are reused from the repository root `skills/` surface; this
|
|
21
|
+
# distribution does not ship its own skills/ tree.
|
|
22
|
+
skills: ../skills
|
|
23
|
+
|
|
24
|
+
# Environment variables the operator must supply locally before launch.
|
|
25
|
+
# Declared here for documentation only; values are never bundled. The actual
|
|
26
|
+
# secrets live in the operator's own untracked .env (NOT shipped).
|
|
27
|
+
env_requires:
|
|
28
|
+
- name: ANTHROPIC_API_KEY
|
|
29
|
+
required: true
|
|
30
|
+
description: API key for the tutor model provider. Operator-supplied; never bundled.
|
|
31
|
+
|
|
32
|
+
# Hard exclusions: data that must never be packaged, copied, or published.
|
|
33
|
+
exclude:
|
|
34
|
+
- ".env"
|
|
35
|
+
- "*.env"
|
|
36
|
+
- secrets
|
|
37
|
+
- memories
|
|
38
|
+
- sessions
|
|
39
|
+
- "*.sqlite"
|
|
40
|
+
- "*.sqlite3"
|
|
41
|
+
- "*.db"
|
|
42
|
+
- "*.db-wal"
|
|
43
|
+
- "*.db-shm"
|
|
44
|
+
- logs
|
|
45
|
+
- "*.log"
|
|
46
|
+
- caches
|
|
47
|
+
- "*.cache"
|
|
48
|
+
- local
|
|
49
|
+
- local_overrides
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.openclaw.ai/plugins/manifest.schema.json",
|
|
3
|
+
"id": "language-tutor",
|
|
4
|
+
"name": "Language Tutor",
|
|
5
|
+
"displayName": "Language Tutor",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "Text-only language tutor adapter for OpenClaw. Builds boot context on the first tutor message and exposes core text-modality tutor tools.",
|
|
8
|
+
"entry": "dist/index.js",
|
|
9
|
+
"activation": {
|
|
10
|
+
"onStartup": true
|
|
11
|
+
},
|
|
12
|
+
"contracts": {
|
|
13
|
+
"tools": ["language_tutor"]
|
|
14
|
+
},
|
|
15
|
+
"configSchema": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {},
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22"
|
|
22
|
+
},
|
|
23
|
+
"capabilities": {
|
|
24
|
+
"text": "supported",
|
|
25
|
+
"audio": "unsupported",
|
|
26
|
+
"image": "unsupported"
|
|
27
|
+
},
|
|
28
|
+
"lifecycle": {
|
|
29
|
+
"start": "first_message",
|
|
30
|
+
"end": "not_available",
|
|
31
|
+
"bootContextTrigger": "first_tutor_message"
|
|
32
|
+
},
|
|
33
|
+
"tools": [
|
|
34
|
+
{
|
|
35
|
+
"name": "language_tutor.boot_context",
|
|
36
|
+
"optional": false
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "language_tutor.text_exercise",
|
|
40
|
+
"optional": false
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "language_tutor.run_cli",
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@language-tutor/openclaw-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw host plugin for the language-tutor text-modality adapter.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"openclaw": {
|
|
19
|
+
"extensions": ["./dist/index.js"]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"openclaw.plugin.json"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"check": "tsc --noEmit -p tsconfig.json"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"openclaw": ">=1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"typebox": "^1.1.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.5.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Language Tutor plugin entry for the OpenClaw host.
|
|
2
|
+
//
|
|
3
|
+
// Uses focused SDK subpath imports (NOT a whole-SDK wildcard import) per the
|
|
4
|
+
// OpenClaw plugin model: https://docs.openclaw.ai/plugins
|
|
5
|
+
//
|
|
6
|
+
// Capability stance (mirrors src/language_tutor/adapters/registry.py OpenClaw
|
|
7
|
+
// defaults): text supported; lifecycle start = first_message; lifecycle end =
|
|
8
|
+
// not_available; boot context is built on the first tutor message. Any
|
|
9
|
+
// side-effectful / binary-dependent tool is registered opt-in only.
|
|
10
|
+
|
|
11
|
+
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
|
|
14
|
+
export default defineToolPlugin({
|
|
15
|
+
id: "language-tutor",
|
|
16
|
+
name: "Language Tutor",
|
|
17
|
+
description:
|
|
18
|
+
"Text-only language tutor adapter for OpenClaw. Builds boot context on the first tutor message and exposes core text-modality tutor tools.",
|
|
19
|
+
tools: (tool) => [
|
|
20
|
+
// First-message boot trigger: OpenClaw has no SessionStart-style hook, so
|
|
21
|
+
// the tutor assembles boot context the first time the learner messages
|
|
22
|
+
// the tutor.
|
|
23
|
+
tool({
|
|
24
|
+
name: "language_tutor.boot_context",
|
|
25
|
+
description:
|
|
26
|
+
"Assemble learner boot context (profile, preferences, focus) on the first tutor message.",
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
sessionId: Type.Optional(Type.String()),
|
|
29
|
+
}),
|
|
30
|
+
async execute(_params, _config, _context) {
|
|
31
|
+
// Pure text assembly only. No persistence or host-specific behavior
|
|
32
|
+
// here; the core tutor owns boot-context generation.
|
|
33
|
+
return { sections: [] as string[] };
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
// Text-modality tutor tool (reading / lesson / transcript / vocab /
|
|
37
|
+
// writing / progress are all text-only flows). No side effects beyond
|
|
38
|
+
// returning text.
|
|
39
|
+
tool({
|
|
40
|
+
name: "language_tutor.text_exercise",
|
|
41
|
+
description:
|
|
42
|
+
"Present and evaluate a text-only tutor exercise (reading, lesson, transcript, vocab, writing, progress).",
|
|
43
|
+
parameters: Type.Object({
|
|
44
|
+
modality: Type.String(),
|
|
45
|
+
payload: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
46
|
+
}),
|
|
47
|
+
async execute(params, _config, _context) {
|
|
48
|
+
return { modality: params.modality, text: "" };
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
// Side-effectful / binary-dependent tool. Shells out to the local tutor
|
|
52
|
+
// CLI, so it is OPT-IN ONLY and gated behind a user allowlist.
|
|
53
|
+
tool({
|
|
54
|
+
name: "language_tutor.run_cli",
|
|
55
|
+
description:
|
|
56
|
+
"Invoke the local language-tutor CLI. Binary-dependent and side-effectful; opt-in only.",
|
|
57
|
+
optional: true,
|
|
58
|
+
parameters: Type.Object({
|
|
59
|
+
command: Type.Array(Type.String()),
|
|
60
|
+
}),
|
|
61
|
+
async execute(params, _config, _context) {
|
|
62
|
+
// Execution is performed by the host only after the user allowlists
|
|
63
|
+
// it.
|
|
64
|
+
return { command: params.command };
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src"]
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Host adapter boundaries."""
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
from language_tutor.schemas import (
|
|
6
|
+
APPROVED_HOST_SOURCES,
|
|
7
|
+
AdapterCapabilityProfile,
|
|
8
|
+
BootContextTrigger,
|
|
9
|
+
ConformanceRun,
|
|
10
|
+
HostId,
|
|
11
|
+
HostSetupProfileContract,
|
|
12
|
+
HostSetupTarget,
|
|
13
|
+
ManualProviderInstallReport,
|
|
14
|
+
SetupModel,
|
|
15
|
+
SetupPackage,
|
|
16
|
+
TargetStatus,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class JsonCommandRunner(Protocol):
|
|
21
|
+
def run_json(
|
|
22
|
+
self, args: list[str], payload: dict[str, object] | None = None
|
|
23
|
+
) -> dict[str, object]:
|
|
24
|
+
"""Run a host-facing JSON command."""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HookPayloadAdapter(Protocol):
|
|
29
|
+
def normalize(self, payload: dict[str, object]) -> dict[str, object]:
|
|
30
|
+
"""Normalize host hook payload into core JSON."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HostCapabilityProvider(Protocol):
|
|
35
|
+
def capability_profile(self) -> AdapterCapabilityProfile:
|
|
36
|
+
"""Return the host's declared capability profile."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LifecycleTriggerProvider(Protocol):
|
|
41
|
+
def boot_trigger(self) -> BootContextTrigger:
|
|
42
|
+
"""Return the host's boot-context lifecycle trigger."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SetupPackageProvider(Protocol):
|
|
47
|
+
def setup_package(self) -> SetupPackage:
|
|
48
|
+
"""Describe the host-owned distribution package."""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ConformanceRunner(Protocol):
|
|
53
|
+
def run_conformance(self) -> ConformanceRun:
|
|
54
|
+
"""Run shared host-portability conformance checks."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ManualReportProvider(Protocol):
|
|
59
|
+
def manual_report(self) -> ManualProviderInstallReport:
|
|
60
|
+
"""Return the host's manual provider install report."""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Supported host target registry (US1). Single source of truth for the four
|
|
65
|
+
# approved hosts, their official setup-doc sources, setup models, and contract
|
|
66
|
+
# paths. Antigravity is intentionally absent and rejected at the schema layer.
|
|
67
|
+
_REGISTRY: dict[HostId, HostSetupTarget] = {
|
|
68
|
+
HostId.HERMES: HostSetupTarget(
|
|
69
|
+
id=HostId.HERMES,
|
|
70
|
+
display_name="Hermes",
|
|
71
|
+
official_source_url=APPROVED_HOST_SOURCES[HostId.HERMES.value],
|
|
72
|
+
setup_model=SetupModel.PROFILE_DISTRIBUTION,
|
|
73
|
+
primary_subagent="hermes-adapter-subagent",
|
|
74
|
+
contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/hermes.md",
|
|
75
|
+
status=TargetStatus.PLANNED,
|
|
76
|
+
),
|
|
77
|
+
HostId.OPENCLAW: HostSetupTarget(
|
|
78
|
+
id=HostId.OPENCLAW,
|
|
79
|
+
display_name="OpenClaw",
|
|
80
|
+
official_source_url=APPROVED_HOST_SOURCES[HostId.OPENCLAW.value],
|
|
81
|
+
setup_model=SetupModel.PLUGIN_PACKAGE,
|
|
82
|
+
primary_subagent="openclaw-adapter-subagent",
|
|
83
|
+
contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/openclaw.md",
|
|
84
|
+
status=TargetStatus.PLANNED,
|
|
85
|
+
),
|
|
86
|
+
HostId.CLAUDE: HostSetupTarget(
|
|
87
|
+
id=HostId.CLAUDE,
|
|
88
|
+
display_name="Claude",
|
|
89
|
+
official_source_url=APPROVED_HOST_SOURCES[HostId.CLAUDE.value],
|
|
90
|
+
setup_model=SetupModel.PLUGIN_PACKAGE,
|
|
91
|
+
primary_subagent="claude-adapter-subagent",
|
|
92
|
+
contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/claude.md",
|
|
93
|
+
status=TargetStatus.PLANNED,
|
|
94
|
+
),
|
|
95
|
+
HostId.CODEX: HostSetupTarget(
|
|
96
|
+
id=HostId.CODEX,
|
|
97
|
+
display_name="Codex",
|
|
98
|
+
official_source_url=APPROVED_HOST_SOURCES[HostId.CODEX.value],
|
|
99
|
+
setup_model=SetupModel.LOCAL_MARKETPLACE_PLUGIN,
|
|
100
|
+
primary_subagent="codex-adapter-subagent",
|
|
101
|
+
contract_path="specs/006-agent-adapter-setup/contracts/host-setup-profiles/codex.md",
|
|
102
|
+
status=TargetStatus.PLANNED,
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def supported_host_targets() -> dict[HostId, HostSetupTarget]:
|
|
108
|
+
"""Return the registry of approved host setup targets."""
|
|
109
|
+
return dict(_REGISTRY)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def is_supported_host(host_id: str) -> bool:
|
|
113
|
+
"""True only for hermes, openclaw, claude, codex. Antigravity is rejected."""
|
|
114
|
+
return host_id in {h.value for h in HostId}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_host_target(host_id: str) -> HostSetupTarget:
|
|
118
|
+
return _REGISTRY[HostId(host_id)]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def normalize_hook_payload(payload: dict[str, object]) -> dict[str, object]:
|
|
122
|
+
return dict(payload)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def run_conformance(profile: AdapterCapabilityProfile) -> ConformanceRun:
|
|
126
|
+
"""Build a deterministic conformance result from a host capability profile.
|
|
127
|
+
|
|
128
|
+
The runner is host-blind: it derives the six representative-flow outcomes
|
|
129
|
+
from the declared capability profile (a gated flow is ``skipped``, every
|
|
130
|
+
other flow ``pass``) and reports the shared boot/feedback/progress/error/
|
|
131
|
+
data-ownership contract results. It owns no pedagogy or learner state.
|
|
132
|
+
"""
|
|
133
|
+
from language_tutor.schemas import (
|
|
134
|
+
ConformanceRun,
|
|
135
|
+
Decision,
|
|
136
|
+
FlowResult,
|
|
137
|
+
RepresentativeFlow,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
gated = {str(g) for g in profile.flow_gates}
|
|
141
|
+
flows: dict[RepresentativeFlow, FlowResult] = {}
|
|
142
|
+
for flow in RepresentativeFlow:
|
|
143
|
+
flows[flow] = FlowResult.SKIPPED if flow.value in gated else FlowResult.PASS
|
|
144
|
+
|
|
145
|
+
skipped = [f for f in RepresentativeFlow if f.value in gated]
|
|
146
|
+
has_fail = any(r == FlowResult.FAIL for r in flows.values())
|
|
147
|
+
decision = Decision.FAIL if has_fail else Decision.PASS
|
|
148
|
+
|
|
149
|
+
return ConformanceRun(
|
|
150
|
+
host=HostId(str(profile.host)),
|
|
151
|
+
capability_profile="schemas/host_capability_profile.schema.json",
|
|
152
|
+
flows=flows,
|
|
153
|
+
boot_context_result=FlowResult.PASS,
|
|
154
|
+
feedback_contract_result=FlowResult.PASS,
|
|
155
|
+
progress_contract_result=FlowResult.PASS,
|
|
156
|
+
error_behavior_result=FlowResult.PASS,
|
|
157
|
+
data_ownership_result=FlowResult.PASS,
|
|
158
|
+
skipped_flows=skipped,
|
|
159
|
+
decision=decision,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def load_host_setup_profile(path: str) -> HostSetupProfileContract:
|
|
164
|
+
"""Load and validate a host setup profile markdown contract.
|
|
165
|
+
|
|
166
|
+
The markdown contract embeds a fenced ```json block describing the
|
|
167
|
+
HostSetupProfileContract fields. This loader extracts and validates it.
|
|
168
|
+
"""
|
|
169
|
+
import json
|
|
170
|
+
import re
|
|
171
|
+
from pathlib import Path
|
|
172
|
+
|
|
173
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
174
|
+
match = re.search(r"```json\s*(\{.*?\})\s*```", text, re.DOTALL)
|
|
175
|
+
if not match:
|
|
176
|
+
raise ValueError(f"host setup profile {path} has no json contract block")
|
|
177
|
+
data = json.loads(match.group(1))
|
|
178
|
+
return HostSetupProfileContract.model_validate(data)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from language_tutor.adapters.registry import capability_profile_for
|
|
4
|
+
from language_tutor.schemas import AdapterCapabilityProfile, HostId
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def capability_profile() -> AdapterCapabilityProfile:
|
|
8
|
+
"""Claude capability profile. No-hook lifecycle (spec 007)."""
|
|
9
|
+
return capability_profile_for(HostId.CLAUDE.value)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def plugin_root_components() -> dict[str, str]:
|
|
13
|
+
return {
|
|
14
|
+
"manifest": ".claude-plugin/plugin.json",
|
|
15
|
+
"setup_skill": "skills/tutor-setup/SKILL.md",
|
|
16
|
+
"vocab_skill": "skills/tutor-vocab/SKILL.md",
|
|
17
|
+
"writing_skill": "skills/tutor-writing/SKILL.md",
|
|
18
|
+
"progress_skill": "skills/tutor-progress/SKILL.md",
|
|
19
|
+
"judge_agent": "agents/tutor-judge.md",
|
|
20
|
+
"cli": "bin/tutor",
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Codex host adapter.
|
|
2
|
+
|
|
3
|
+
Codex distributes as a local-marketplace plugin and boots the tutor on the
|
|
4
|
+
first tutor-skill invocation (no plugin hook required). Setup is package-only.
|
|
5
|
+
This module exposes the host capability profile and leaves pedagogy, feedback,
|
|
6
|
+
progress, and learner state to the tutor core.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from language_tutor.adapters.registry import capability_profile_for
|
|
12
|
+
from language_tutor.schemas import AdapterCapabilityProfile, HostId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def capability_profile() -> AdapterCapabilityProfile:
|
|
16
|
+
return capability_profile_for(HostId.CODEX.value)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Hermes host adapter.
|
|
2
|
+
|
|
3
|
+
Hermes uses a git-backed profile distribution and has no Claude-style startup
|
|
4
|
+
hook, so it boots the tutor through an explicit command trigger. Setup is
|
|
5
|
+
package-only; this module exposes the host capability profile and leaves all
|
|
6
|
+
pedagogy, feedback, progress, and learner state to the tutor core.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from language_tutor.adapters.registry import capability_profile_for
|
|
12
|
+
from language_tutor.schemas import AdapterCapabilityProfile, HostId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def capability_profile() -> AdapterCapabilityProfile:
|
|
16
|
+
return capability_profile_for(HostId.HERMES.value)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""OpenClaw host adapter.
|
|
2
|
+
|
|
3
|
+
OpenClaw distributes as a Node/TypeScript ESM plugin package and has no
|
|
4
|
+
Claude-style startup hook, so it boots the tutor on the first tutor message.
|
|
5
|
+
Setup is package-only; this module exposes the host capability profile and
|
|
6
|
+
leaves pedagogy, feedback, progress, and learner state to the tutor core.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from language_tutor.adapters.registry import capability_profile_for
|
|
12
|
+
from language_tutor.schemas import AdapterCapabilityProfile, HostId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def capability_profile() -> AdapterCapabilityProfile:
|
|
16
|
+
return capability_profile_for(HostId.OPENCLAW.value)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Host capability profile defaults (single source of truth).
|
|
2
|
+
|
|
3
|
+
Each supported host declares its capability profile here. Host-specific adapter
|
|
4
|
+
modules (claude.py, codex.py, hermes.py, openclaw.py) may translate runtime
|
|
5
|
+
behavior, but capability declarations stay centralized to keep flow names,
|
|
6
|
+
lifecycle values, and boot triggers DRY.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from language_tutor.schemas import (
|
|
12
|
+
AdapterCapabilityProfile,
|
|
13
|
+
BootTrigger,
|
|
14
|
+
CapabilitySupport,
|
|
15
|
+
HostId,
|
|
16
|
+
LifecycleEnd,
|
|
17
|
+
LifecycleStart,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_DEFAULTS: dict[HostId, AdapterCapabilityProfile] = {
|
|
21
|
+
HostId.CLAUDE: AdapterCapabilityProfile(
|
|
22
|
+
host=HostId.CLAUDE,
|
|
23
|
+
display_name="Claude",
|
|
24
|
+
text_support=CapabilitySupport.SUPPORTED,
|
|
25
|
+
lifecycle_start=LifecycleStart.FIRST_MESSAGE,
|
|
26
|
+
lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
|
|
27
|
+
boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
|
|
28
|
+
setup_entry_point="claude --plugin-dir <plugin-root>",
|
|
29
|
+
update_behavior="/reload-plugins",
|
|
30
|
+
side_effectful_capabilities=[],
|
|
31
|
+
unsupported_capabilities=["audio", "image"],
|
|
32
|
+
flow_gates=[],
|
|
33
|
+
),
|
|
34
|
+
HostId.CODEX: AdapterCapabilityProfile(
|
|
35
|
+
host=HostId.CODEX,
|
|
36
|
+
display_name="Codex",
|
|
37
|
+
text_support=CapabilitySupport.SUPPORTED,
|
|
38
|
+
lifecycle_start=LifecycleStart.FIRST_MESSAGE,
|
|
39
|
+
lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
|
|
40
|
+
boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
|
|
41
|
+
setup_entry_point="local marketplace install via .codex-plugin/plugin.json",
|
|
42
|
+
update_behavior="Codex restart/reload after marketplace update",
|
|
43
|
+
side_effectful_capabilities=[],
|
|
44
|
+
unsupported_capabilities=["audio", "image"],
|
|
45
|
+
flow_gates=[],
|
|
46
|
+
),
|
|
47
|
+
HostId.HERMES: AdapterCapabilityProfile(
|
|
48
|
+
host=HostId.HERMES,
|
|
49
|
+
display_name="Hermes",
|
|
50
|
+
text_support=CapabilitySupport.SUPPORTED,
|
|
51
|
+
lifecycle_start=LifecycleStart.FIRST_MESSAGE,
|
|
52
|
+
lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
|
|
53
|
+
boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
|
|
54
|
+
setup_entry_point="hermes profile install",
|
|
55
|
+
update_behavior="hermes profile update",
|
|
56
|
+
side_effectful_capabilities=[],
|
|
57
|
+
unsupported_capabilities=["audio", "image"],
|
|
58
|
+
flow_gates=[],
|
|
59
|
+
),
|
|
60
|
+
HostId.OPENCLAW: AdapterCapabilityProfile(
|
|
61
|
+
host=HostId.OPENCLAW,
|
|
62
|
+
display_name="OpenClaw",
|
|
63
|
+
text_support=CapabilitySupport.SUPPORTED,
|
|
64
|
+
lifecycle_start=LifecycleStart.FIRST_MESSAGE,
|
|
65
|
+
lifecycle_end=LifecycleEnd.NOT_AVAILABLE,
|
|
66
|
+
boot_context_trigger=BootTrigger.FIRST_TUTOR_MESSAGE,
|
|
67
|
+
setup_entry_point="openclaw plugins install <package-name>",
|
|
68
|
+
update_behavior="reinstall/upgrade plugin package",
|
|
69
|
+
side_effectful_capabilities=["binary-dependent tools (opt-in via user allowlist)"],
|
|
70
|
+
unsupported_capabilities=["audio", "image"],
|
|
71
|
+
flow_gates=[],
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def default_capability_profiles() -> dict[HostId, AdapterCapabilityProfile]:
|
|
77
|
+
return dict(_DEFAULTS)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def capability_profile_for(host_id: str) -> AdapterCapabilityProfile:
|
|
81
|
+
return _DEFAULTS[HostId(host_id)]
|