@neurodock/cli 0.8.1 → 0.9.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 (36) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +45 -11
  3. package/dist/assets/hooks/proactive_guardrail.py +11 -23
  4. package/dist/assets/schemas/neurotype-addenda.schema.json +167 -0
  5. package/dist/assets/schemas/profile.example.yaml +9 -0
  6. package/dist/assets/schemas/profile.schema.json +116 -0
  7. package/dist/assets/skills/adhd-daily-planner/SKILL.md +131 -0
  8. package/dist/assets/skills/asd-meeting-translator/SKILL.md +177 -0
  9. package/dist/assets/skills/audhd-context-recovery/SKILL.md +93 -0
  10. package/dist/assets/skills/dyspraxia-task-pacer/SKILL.md +174 -0
  11. package/dist/assets/skills/hyperfocus-formatter/SKILL.md +89 -0
  12. package/dist/assets/skills/ocd-decision-finalizer/SKILL.md +109 -0
  13. package/dist/assets/skills/visual-organizer/SKILL.md +94 -0
  14. package/dist/commands/host.d.ts.map +1 -1
  15. package/dist/commands/host.js +4 -6
  16. package/dist/commands/host.js.map +1 -1
  17. package/dist/commands/install-all.d.ts +27 -0
  18. package/dist/commands/install-all.d.ts.map +1 -1
  19. package/dist/commands/install-all.js +53 -1
  20. package/dist/commands/install-all.js.map +1 -1
  21. package/dist/commands/install-skills.d.ts +26 -0
  22. package/dist/commands/install-skills.d.ts.map +1 -0
  23. package/dist/commands/install-skills.js +164 -0
  24. package/dist/commands/install-skills.js.map +1 -0
  25. package/dist/commands/setup.d.ts +12 -0
  26. package/dist/commands/setup.d.ts.map +1 -1
  27. package/dist/commands/setup.js +2 -0
  28. package/dist/commands/setup.js.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +37 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/skills.d.ts +40 -0
  33. package/dist/lib/skills.d.ts.map +1 -0
  34. package/dist/lib/skills.js +115 -0
  35. package/dist/lib/skills.js.map +1 -0
  36. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @neurodock/cli changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1c76c2c: add `neurodock install-skills` and bundle the per-neurotype skills into the cli
8
+
9
+ the cli now ships the six per-neurotype skills (their `SKILL.md` files,
10
+ generated into `dist/assets/skills/` at build time from `packages/skills/`).
11
+ the new `neurodock install-skills` command copies them into the client's
12
+ personal skills directory (`~/.claude/skills/neurodock-<name>/` for claude code
13
+ and claude desktop; cursor is skipped). it supports `--client`, `--dry-run`,
14
+ and `--yes`, and is idempotent.
15
+
16
+ `install-all`, `setup`, and `update` now install the skills as part of the
17
+ one-command happy path; opt out with `--no-skills` (mirrors `--no-native-host`).
18
+ the skills step is best-effort — a failure warns but does not fail the command.
19
+
20
+ ## 0.8.2
21
+
22
+ ### Patch Changes
23
+
24
+ - a7c3f01: fix(native-host): register the real published extension ids so the extension can connect
25
+
26
+ The native-messaging host was registered with the literal placeholder
27
+ `__NEURODOCK_EXTENSION_ID__`, which was never substituted — so the host
28
+ manifest's `allowed_origins` matched no extension and the browser
29
+ extension always showed "Not connected yet" even after a successful
30
+ `setup`/`install-all`.
31
+
32
+ - The published Chrome Web Store id (`lcdaiekokkgniiknejddojkfkoiinopo`)
33
+ and the Firefox gecko id (`neurodock-extension@neurodock.org`) are now
34
+ the registered defaults (`PUBLISHED_EXTENSION_IDS` /
35
+ `withDefaultExtensionIds` in `@neurodock/native-host`), so a
36
+ store-installed extension connects out of the box.
37
+ - `neurodock setup` and `neurodock install-all` gained a repeatable
38
+ `--extension-id <id>` flag, threaded down to the host installer, so a
39
+ locally-loaded unpacked build (whose id differs from the published one)
40
+ can be allowed in one command. Provided ids are unioned with the
41
+ published defaults and deduped.
42
+
43
+ - Updated dependencies [a7c3f01]
44
+ - @neurodock/native-host@0.2.1
45
+
3
46
  ## 0.8.1
4
47
 
5
48
  ### Patch Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `neurodock` installer and diagnostic CLI for [NeuroDock](https://neurodock.org/) — a local-first cognitive substrate for neurodivergent professionals.
4
4
 
5
- Status: **v0.6.2**.
5
+ Status: **v0.8.2**.
6
6
 
7
7
  ## Quickstart
8
8
 
@@ -34,8 +34,9 @@ install, `install-all` runs the `pip install` step automatically.
34
34
 
35
35
  | Command | What it does |
36
36
  | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
- | `neurodock install-all` | One-command first-time install: pip-install the six servers, wire every detected client, copy the starter profile. |
37
+ | `neurodock install-all` | One-command first-time install: pip-install the six servers, wire every detected client, copy the starter profile, install the per-neurotype skills. |
38
38
  | `neurodock init` | Install MCP servers into Claude Desktop / Claude Code / Cursor (the wiring half of `install-all`). |
39
+ | `neurodock install-skills` | Copy the per-neurotype skills into your client's personal skills directory (`~/.claude/skills` for Claude Code / Claude Desktop). Cursor is skipped. |
39
40
  | `neurodock doctor` | Diagnose your install — profile validity, client wiring, tool availability. |
40
41
  | `neurodock validate` | Schema-validate a profile file (`~/.neurodock/profile.yaml` by default). |
41
42
  | `neurodock update` | Upgrade NeuroDock to the latest version — re-installs the six MCP servers via pip/uv and re-wires client configs. |
@@ -60,16 +61,47 @@ install, `install-all` runs the `pip install` step automatically.
60
61
  neurodock install-all [--client=claude-desktop|claude-code|cursor|all] \
61
62
  [--profile=minimal|example] \
62
63
  [--installer=uv|pip|auto] \
63
- [--skip-install] [--yes] [--dry-run]
64
+ [--skip-install] [--yes] [--dry-run] \
65
+ [--no-native-host] [--no-skills]
64
66
  ```
65
67
 
66
68
  Single-command first-time install. Prefers `uv` if it is on PATH; falls
67
69
  back to `python -m pip`. After install, verifies each server entrypoint
68
- is on PATH with `<command> --help`, then delegates to `init` to wire
69
- clients.
70
+ is on PATH with `<command> --help`, delegates to `init` to wire clients,
71
+ registers the optional native-messaging host, and installs the
72
+ per-neurotype skills into `~/.claude/skills`.
70
73
 
71
- Exit codes: `0` ok, `1` an entrypoint is missing from PATH after
72
- install, `2` init failed.
74
+ - `--no-native-host` skips registering the native-messaging host.
75
+ - `--no-skills` skips copying the per-neurotype skills.
76
+
77
+ Both steps are best-effort: a failure in either emits a warning but does
78
+ not fail the command (the six MCP servers are the core). Exit codes:
79
+ `0` ok, `1` an entrypoint is missing from PATH after install, `2` init
80
+ failed.
81
+
82
+ ### `neurodock install-skills`
83
+
84
+ ```
85
+ neurodock install-skills [--client=claude-desktop|claude-code|cursor|all] \
86
+ [--dry-run] [--yes]
87
+ ```
88
+
89
+ Copies the six per-neurotype skills bundled in the CLI tarball into the
90
+ client's personal skills directory so Claude Code / Claude Desktop
91
+ discover them. Each skill installs as
92
+ `~/.claude/skills/neurodock-<name>/SKILL.md` (namespaced so it is
93
+ collision-free and recognisably NeuroDock's).
94
+
95
+ - Claude Code and Claude Desktop share `~/.claude/skills`; the command
96
+ de-duplicates so the files are written once.
97
+ - Cursor has no skills system today and is skipped with a notice (never
98
+ an error).
99
+ - `--dry-run` prints the planned targets and exits 0 without writing.
100
+ - Idempotent: re-running refreshes each `SKILL.md` in place.
101
+
102
+ This is also run automatically by `install-all` / `setup` / `update`
103
+ unless you pass `--no-skills`. Exit codes: `0` ok, `1` no bundled skills
104
+ were found (a packaging bug).
73
105
 
74
106
  ### `neurodock init`
75
107
 
@@ -100,14 +132,16 @@ key are skipped unless `--yes` is supplied.
100
132
  neurodock update [--client=claude-desktop|claude-code|cursor|all] \
101
133
  [--profile=minimal|example] \
102
134
  [--installer=uv|pip|auto] \
103
- [--skip-install] [--yes] [--dry-run] [--no-native-host]
135
+ [--skip-install] [--yes] [--dry-run] \
136
+ [--no-native-host] [--no-skills]
104
137
  ```
105
138
 
106
139
  One-command upgrade. Same code path as `install-all` — re-runs
107
140
  `pip install --upgrade` (or `uv tool install`) for every NeuroDock MCP
108
- server, re-wires the detected MCP clients, and re-registers the
109
- optional native-messaging host. Exit codes match `install-all`:
110
- `0` ok, `1` an entrypoint is missing from PATH, `2` init failed.
141
+ server, re-wires the detected MCP clients, re-registers the optional
142
+ native-messaging host, and refreshes the per-neurotype skills (skip with
143
+ `--no-skills`). Exit codes match `install-all`: `0` ok, `1` an
144
+ entrypoint is missing from PATH, `2` init failed.
111
145
 
112
146
  ### `neurodock sync`
113
147
 
@@ -174,9 +174,7 @@ def _on_session_start(_payload: dict[str, Any], settings: dict[str, Any]) -> Non
174
174
  state["tool_count"] = 0
175
175
  _save_session(state)
176
176
  band = _clock_band(now)
177
- break_minutes = settings.get(
178
- "hyperfocus_break_minutes", HYPERFOCUS_BREAK_MINUTES_DEFAULT
179
- )
177
+ break_minutes = settings.get("hyperfocus_break_minutes", HYPERFOCUS_BREAK_MINUTES_DEFAULT)
180
178
  if band in ("deep_night", "late_night"):
181
179
  _emit_banner(
182
180
  f"NeuroDock: it's {band.replace('_', ' ')} local time. "
@@ -208,17 +206,13 @@ def _on_pre_tool(payload: dict[str, Any], settings: dict[str, Any]) -> None:
208
206
  if state["tool_count"] % PRETOOL_CHECK_EVERY_N != 0:
209
207
  return
210
208
 
211
- break_minutes = settings.get(
212
- "hyperfocus_break_minutes", HYPERFOCUS_BREAK_MINUTES_DEFAULT
213
- )
209
+ break_minutes = settings.get("hyperfocus_break_minutes", HYPERFOCUS_BREAK_MINUTES_DEFAULT)
214
210
  end_of_day = settings.get("end_of_day_local")
215
211
  hyperfocus_banner = _evaluate_hyperfocus(state, break_minutes, end_of_day)
216
212
  if hyperfocus_banner:
217
213
  _emit_banner(hyperfocus_banner)
218
214
  threshold = settings.get("rumination_threshold", RUMINATION_THRESHOLD_DEFAULT)
219
- window = settings.get(
220
- "rumination_window_minutes", RUMINATION_WINDOW_MINUTES_DEFAULT
221
- )
215
+ window = settings.get("rumination_window_minutes", RUMINATION_WINDOW_MINUTES_DEFAULT)
222
216
  rumination_banner = _evaluate_rumination(threshold, window)
223
217
  if rumination_banner:
224
218
  _emit_banner(rumination_banner)
@@ -518,7 +512,7 @@ def _is_past_end_of_day(now: datetime, end_of_day_local: str | None) -> bool:
518
512
  hour, minute = int(match.group(1)), int(match.group(2))
519
513
  if not (0 <= hour <= 23 and 0 <= minute <= 59):
520
514
  return _clock_band(now) in ("late_night", "deep_night")
521
- # Deep night (00:0005:59) is always "past end of day" regardless of the
515
+ # Deep night (00:00-05:59) is always "past end of day" regardless of the
522
516
  # configured clock-out — nobody sets end_of_day to 03:00 and means it.
523
517
  if now.hour < 6:
524
518
  return True
@@ -576,21 +570,15 @@ def _parse_profile_text(text: str) -> dict[str, Any]:
576
570
 
577
571
  hyperfocus = _extract_int(text, "hyperfocus_break_minutes")
578
572
  if hyperfocus is not None:
579
- settings["hyperfocus_break_minutes"] = _clamp(
580
- hyperfocus, *HYPERFOCUS_BREAK_MIN_RANGE
581
- )
573
+ settings["hyperfocus_break_minutes"] = _clamp(hyperfocus, *HYPERFOCUS_BREAK_MIN_RANGE)
582
574
 
583
575
  threshold = _extract_int(text, "rumination_threshold")
584
576
  if threshold is not None:
585
- settings["rumination_threshold"] = _clamp(
586
- threshold, *RUMINATION_THRESHOLD_RANGE
587
- )
577
+ settings["rumination_threshold"] = _clamp(threshold, *RUMINATION_THRESHOLD_RANGE)
588
578
 
589
579
  window = _extract_int(text, "rumination_window_minutes")
590
580
  if window is not None:
591
- settings["rumination_window_minutes"] = _clamp(
592
- window, *RUMINATION_WINDOW_RANGE
593
- )
581
+ settings["rumination_window_minutes"] = _clamp(window, *RUMINATION_WINDOW_RANGE)
594
582
 
595
583
  eod = _extract_str(text, "end_of_day_local")
596
584
  if eod is not None and re.match(r"^\d{1,2}:\d{2}$", eod):
@@ -805,15 +793,15 @@ def _self_test() -> int:
805
793
 
806
794
  # Profile parsing: extract scalars from a representative profile snippet.
807
795
  sample_profile = (
808
- "schema_version: \"0.1.0\"\n"
796
+ 'schema_version: "0.1.0"\n'
809
797
  "chronometric:\n"
810
798
  " # how long before a nudge\n"
811
799
  " hyperfocus_break_minutes: 60\n"
812
- " end_of_day_local: \"18:30\"\n"
800
+ ' end_of_day_local: "18:30"\n'
813
801
  "guardrails:\n"
814
802
  " rumination_threshold: 4\n"
815
803
  " rumination_window_minutes: 120\n"
816
- " sycophancy_check: \"off\"\n"
804
+ ' sycophancy_check: "off"\n'
817
805
  )
818
806
  parsed = _parse_profile_text(sample_profile)
819
807
  expected = {
@@ -834,7 +822,7 @@ def _self_test() -> int:
834
822
  ok = False
835
823
 
836
824
  # Profile parsing: an empty / commented profile yields no overrides.
837
- if _parse_profile_text("# just a comment\nschema_version: \"0.1.0\"\n") != {}:
825
+ if _parse_profile_text('# just a comment\nschema_version: "0.1.0"\n') != {}:
838
826
  sys.stderr.write("FAIL: bare profile should yield no overrides\n")
839
827
  ok = False
840
828
 
@@ -0,0 +1,167 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.neurodock.org/neurotype-addenda/v1/neurotype-addenda.schema.json",
4
+ "title": "NeuroDock Neurotype Addenda Artifact",
5
+ "description": "Validates the language-neutral, enum-keyed prompt-shaping content artifact (data/neurotype-addenda/v1.json). This artifact is CONTENT, not schema shape (ADR 0011): it carries the per-(tool x neurotype) prose blocks that NeuroDock surfaces append to a model prompt, plus the fusion rule, priority ordering, framing, output-format guidance, and the cross-cutting voice-input / tourette / other / generic-fallback blocks. Additive-only versioning: new tools, new neurotypes, and new optional top-level keys are non-breaking. Unknown keys are permitted for forward-compatibility where appropriate.",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "required": [
9
+ "artifact_version",
10
+ "fusion",
11
+ "priority",
12
+ "framing",
13
+ "output_format",
14
+ "voice_input",
15
+ "tourette",
16
+ "other",
17
+ "generic",
18
+ "tools"
19
+ ],
20
+ "properties": {
21
+ "$schema": {
22
+ "type": "string",
23
+ "description": "Optional pointer back to this schema's $id."
24
+ },
25
+ "artifact_version": {
26
+ "type": "string",
27
+ "description": "Semantic version of the artifact content. Additive changes bump minor; a breaking re-shape forks to a new vN.json + a new schema $id.",
28
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$",
29
+ "examples": ["1.0.0"]
30
+ },
31
+ "description": {
32
+ "type": "string"
33
+ },
34
+ "tokens": {
35
+ "type": "array",
36
+ "description": "The complete set of interpolation tokens any block may contain. Documentation only; the assembler is the authority.",
37
+ "items": { "type": "string" }
38
+ },
39
+ "fusion": {
40
+ "type": "object",
41
+ "description": "The AuDHD substitution rule. When the input set lists the `result` neurotype directly, OR lists every neurotype in `all_of`, the `result` block is substituted and every neurotype in `remove` is dropped.",
42
+ "additionalProperties": true,
43
+ "required": ["result", "all_of", "remove"],
44
+ "properties": {
45
+ "description": { "type": "string" },
46
+ "result": { "type": "string" },
47
+ "any_of": {
48
+ "type": "array",
49
+ "items": { "type": "string" }
50
+ },
51
+ "all_of": {
52
+ "type": "array",
53
+ "items": { "type": "string" }
54
+ },
55
+ "remove": {
56
+ "type": "array",
57
+ "items": { "type": "string" }
58
+ }
59
+ }
60
+ },
61
+ "priority": {
62
+ "type": "array",
63
+ "description": "Neurotype ordering. Higher-priority addenda are placed LATER in the assembled prompt (recency bias). Neurotypes are sorted by their index in this list.",
64
+ "items": { "type": "string" },
65
+ "minItems": 1
66
+ },
67
+ "framing": {
68
+ "type": "object",
69
+ "description": "The wrapper, header, footer, separators, and conflict footer that surround the per-neurotype blocks.",
70
+ "additionalProperties": true,
71
+ "required": [
72
+ "wrapper_prefix",
73
+ "wrapper_suffix",
74
+ "section_separator",
75
+ "block_line_separator",
76
+ "header",
77
+ "footer",
78
+ "conflict_footer_min_neurotypes",
79
+ "conflict_footer"
80
+ ],
81
+ "properties": {
82
+ "wrapper_prefix": { "type": "string" },
83
+ "wrapper_suffix": { "type": "string" },
84
+ "section_separator": { "type": "string" },
85
+ "block_line_separator": { "type": "string" },
86
+ "header": {
87
+ "type": "array",
88
+ "items": { "type": "string" }
89
+ },
90
+ "footer": {
91
+ "type": "array",
92
+ "items": { "type": "string" }
93
+ },
94
+ "conflict_footer_min_neurotypes": {
95
+ "type": "integer",
96
+ "minimum": 1
97
+ },
98
+ "conflict_footer": { "type": "string" }
99
+ }
100
+ },
101
+ "output_format": {
102
+ "type": "object",
103
+ "description": "Per-output-format guidance. The assembler renders `prefix + format + separator + descriptions[format]`.",
104
+ "additionalProperties": true,
105
+ "required": ["prefix", "separator", "descriptions", "default"],
106
+ "properties": {
107
+ "prefix": { "type": "string" },
108
+ "separator": { "type": "string" },
109
+ "descriptions": {
110
+ "type": "object",
111
+ "description": "Map of output-format enum value to its one-line description.",
112
+ "additionalProperties": { "type": "string" },
113
+ "minProperties": 1
114
+ },
115
+ "default": { "type": "string" }
116
+ }
117
+ },
118
+ "voice_input": {
119
+ "type": "object",
120
+ "description": "Cross-cutting voice-input block. Emitted when voice input is preferred, before the per-neurotype blocks.",
121
+ "additionalProperties": true,
122
+ "required": ["block"],
123
+ "properties": {
124
+ "block": { "$ref": "#/$defs/block" }
125
+ }
126
+ },
127
+ "tourette": {
128
+ "type": "object",
129
+ "description": "Tourette special block. Tool-independent; rendered whenever `tourette` is in the effective set.",
130
+ "additionalProperties": true,
131
+ "required": ["block"],
132
+ "properties": {
133
+ "block": { "$ref": "#/$defs/block" }
134
+ }
135
+ },
136
+ "other": {
137
+ "type": "object",
138
+ "description": "Self-described block. Carries the {notes} interpolation token; rendered when `other` is selected with notes, or appended as a footer when notes are present without `other`.",
139
+ "additionalProperties": true,
140
+ "required": ["block"],
141
+ "properties": {
142
+ "block": { "$ref": "#/$defs/block" }
143
+ }
144
+ },
145
+ "generic": {
146
+ "type": "object",
147
+ "description": "Generic per-neurotype fallback blocks (no tool context, or no concrete per-tool block for the pair). Keyed by neurotype enum value.",
148
+ "additionalProperties": { "$ref": "#/$defs/block" }
149
+ },
150
+ "tools": {
151
+ "type": "object",
152
+ "description": "Per-tool matrices of concrete per-neurotype blocks. Keyed by tool name, then by neurotype enum value. Additive: a new tool or a new neurotype within a tool is non-breaking.",
153
+ "additionalProperties": {
154
+ "type": "object",
155
+ "additionalProperties": { "$ref": "#/$defs/block" }
156
+ }
157
+ }
158
+ },
159
+ "$defs": {
160
+ "block": {
161
+ "type": "array",
162
+ "description": "A prose block: an ordered list of lines joined with the framing block_line_separator. Lines may contain {max_chunk_size} and/or {notes} interpolation tokens.",
163
+ "items": { "type": "string" },
164
+ "minItems": 1
165
+ }
166
+ }
167
+ }
@@ -62,6 +62,15 @@ preferences:
62
62
  # system_default — your OS choice
63
63
  reading_font_hint: "atkinson_hyperlegible"
64
64
 
65
+ # How much space do you want between lines of text?
66
+ # This is a hint for apps that show NeuroDock's output as styled text.
67
+ # Options:
68
+ # compact — lines sit closer together (more text fits on screen)
69
+ # default — the app's own everyday spacing
70
+ # relaxed — extra space between lines; pairs with atkinson_hyperlegible
71
+ # Leave it out and the app just uses its own spacing.
72
+ line_height_hint: "relaxed"
73
+
65
74
  # Do you want visual animations turned on?
66
75
  # Options:
67
76
  # reduced — no animations (default; safest for vestibular sensitivity)
@@ -90,6 +90,17 @@
90
90
  "enum": ["reduced", "system", "full"],
91
91
  "default": "reduced",
92
92
  "description": "Animation policy hint. 'reduced' = no animation, no transitions, no auto-scroll (default). 'system' = follow the OS prefers-reduced-motion setting. 'full' = animations allowed."
93
+ },
94
+ "line_height_hint": {
95
+ "type": "string",
96
+ "enum": ["compact", "default", "relaxed"],
97
+ "description": "Optional (added v0.1.x per ADR 0011). Line-height hint for any client that renders NeuroDock output as HTML or rich text, sitting alongside 'reading_font_hint'. A categorical band rather than a raw multiplier so each client maps it to its own CSS line-height. CONFORMANCE FLOOR: clients MUST NOT render body-paragraph line spacing below 1.5 regardless of band (WCAG 1.4.8 Visual Presentation / 1.4.12 Text Spacing). The bands are advisory line-height ratios relative to font size for body paragraphs; headings and other non-body text MAY be tighter. Per-band anchors: 'compact' ≈ 1.5 — the floor itself, a power-user opt-in for more text per viewport that still respects the 1.5 minimum (for readers who find generous spacing scatters their eye-line and prefer denser text, the dual of why 'relaxed' exists); 'default' ≈ 1.5–1.6 — the client's own comfortable default within the conformant range; 'relaxed' ≈ 1.65–1.8 — generous line spacing (~1.65 is the project body line-height) and the evidence-based pairing with 'atkinson_hyperlegible' for dyslexia, which is why dyslexia-aware presets set it. Read-when-present, neutral-when-absent: an untailored client ignores it; absence is NOT an error. The loader applies no value when absent. Self-ID never gates this (ADR 0004/0011).",
98
+ "examples": ["relaxed", "default"]
99
+ },
100
+ "voice_input_preferred": {
101
+ "type": "boolean",
102
+ "description": "Optional (added v0.1.x per ADR 0011). When true, the user predominantly dictates rather than types for sustained work (common for dyspraxic users). Downstream skills that emit code or structured text MUST NOT assume the user can hand-edit fiddly punctuation cheaply: keep examples copy-pasteable as a single block rather than scattered across inline edits. Read by the shaping layer; absence means 'no preference' and is the same as today's behaviour. Consumer pending; schema-only in this release.",
103
+ "examples": [true]
93
104
  }
94
105
  }
95
106
  },
@@ -121,6 +132,62 @@
121
132
  "enum": ["auto_close", "error"],
122
133
  "default": "auto_close",
123
134
  "description": "Behaviour of mark_session_start when a prior session is still open. 'auto_close' (default, charitable) closes the prior session and returns its metadata so the skill can surface it. 'error' refuses to start a new session until the prior one is explicitly closed. See ADR 0001, open question 3."
135
+ },
136
+ "calendar_phase": {
137
+ "type": "string",
138
+ "enum": ["teaching", "marking", "exam", "deadlines", "break"],
139
+ "description": "Optional (added v0.1.x per ADR 0011). Self-declared phase of the user's term/semester so skills can shift defaults across the calendar: tighter break cadence during 'marking' / 'exam', deadline-cluster awareness during 'deadlines' (e.g. week 12), looser during 'break'. Surfaced by the educator-semester and student-university presets. The neurotype is never a branch point (ADR 0004/0011); this is a user input the shaping layer reads, neutral-when-absent. Consumer pending (mcp-chronometric); schema-only in this release.",
140
+ "examples": ["teaching", "marking"]
141
+ },
142
+ "weekday_overrides": {
143
+ "type": "object",
144
+ "description": "Optional (added v0.1.x per ADR 0011). Per-weekday overrides for 'end_of_day_local' and 'hyperfocus_break_minutes', covering late-office-hours, Wednesday-afternoon-class, and library-day-vs-lecture-day patterns without forcing a whole-profile swap. Keys are lowercase English weekday names; only the seven weekdays are accepted (a misspelt key is a silent no-op, so unknown keys are rejected here rather than preserved). Each value is an override object reusing the same patterns/ranges as the top-level chronometric fields; an empty override object is valid. A weekday absent from the map inherits the top-level chronometric values. Consumer pending (mcp-chronometric); schema-only in this release.",
145
+ "additionalProperties": false,
146
+ "properties": {
147
+ "monday": { "$ref": "#/$defs/weekdayOverride" },
148
+ "tuesday": { "$ref": "#/$defs/weekdayOverride" },
149
+ "wednesday": { "$ref": "#/$defs/weekdayOverride" },
150
+ "thursday": { "$ref": "#/$defs/weekdayOverride" },
151
+ "friday": { "$ref": "#/$defs/weekdayOverride" },
152
+ "saturday": { "$ref": "#/$defs/weekdayOverride" },
153
+ "sunday": { "$ref": "#/$defs/weekdayOverride" }
154
+ },
155
+ "examples": [
156
+ {
157
+ "wednesday": { "end_of_day_local": "18:30" },
158
+ "saturday": { "hyperfocus_break_minutes": 120 }
159
+ }
160
+ ]
161
+ },
162
+ "protected_windows": {
163
+ "type": "array",
164
+ "description": "Optional (added v0.1.x per ADR 0011). Local-time ranges (lunch, post-EOD evening, scheduled lectures, lab times) where the hyperfocus monitor should HARD-SURFACE rather than nudge — the strict rung of the escalation ladder, applied because the window itself is protected, not because a session has run long. Each entry is a 'HH:MM'-'HH:MM' range with an optional human label; ranges are interpreted in the user's local timezone and a range whose 'end' is earlier than its 'start' is treated as wrapping past midnight by the consumer. An empty list is valid. Consumer pending (mcp-chronometric); schema-only in this release.",
165
+ "items": { "$ref": "#/$defs/protectedWindow" },
166
+ "examples": [
167
+ [
168
+ { "start": "12:00", "end": "12:30", "label": "lunch" },
169
+ { "start": "17:00", "end": "23:59", "label": "evening" }
170
+ ]
171
+ ]
172
+ },
173
+ "deadline_cluster_awareness": {
174
+ "type": "boolean",
175
+ "description": "Optional (added v0.1.x per ADR 0011). When true, planning skills surface deadline proximity and shift the break-cadence math when work clusters (e.g. three assignments due in a single week). Set by the student-university preset. Read by planning skills; absence means today's behaviour (no clustering adjustment). Consumer pending (task-fractionator); schema-only in this release.",
176
+ "examples": [true]
177
+ },
178
+ "time_buffer_multiplier": {
179
+ "type": "number",
180
+ "minimum": 1.0,
181
+ "maximum": 3.0,
182
+ "default": 1.0,
183
+ "description": "Optional (added v0.1.x per ADR 0011). Multiplier that pads presented time estimates because real-world execution time for motor-heavy tasks is systematically underestimated (e.g. 1.3 = +30%, the value the dyspraxia preset sets). Range 1.0..3.0; the neutral default 1.0 reproduces today's unpadded behaviour so an untouched profile is unchanged. Read by planning skills to scale the estimates they present; the underlying estimate is not altered. Consumer pending (task-fractionator); schema-only in this release.",
184
+ "examples": [1.0, 1.3]
185
+ },
186
+ "motor_fatigue_aware": {
187
+ "type": "boolean",
188
+ "default": false,
189
+ "description": "Optional (added v0.1.x per ADR 0011). When true, the hyperfocus monitor weights motor activity (click / keystroke / window-switch volume) into the fatigue signal rather than relying on continuous-session length alone — cognitive sharpness and motor exhaustion can coexist (set by the dyspraxia preset). The neutral default false reproduces today's session-length-only behaviour. Reading motor activity is still gated by 'privacy.os_idle_consent' and the relevant OS-input consents; this flag only declares the preference. Consumer pending (mcp-chronometric); schema-only in this release.",
190
+ "examples": [true]
124
191
  }
125
192
  }
126
193
  },
@@ -176,6 +243,55 @@
176
243
  }
177
244
  }
178
245
  },
246
+ "$defs": {
247
+ "weekdayOverride": {
248
+ "type": "object",
249
+ "description": "Per-weekday override for the chronometric fields that vary by day. Both members are optional and reuse the same validation as their top-level counterparts; an empty object means 'this weekday is named but inherits the top-level values'. additionalProperties is false because an unknown key here is almost always a typo that would silently do nothing.",
250
+ "additionalProperties": false,
251
+ "properties": {
252
+ "end_of_day_local": {
253
+ "type": "string",
254
+ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
255
+ "description": "Override of 'chronometric.end_of_day_local' for this weekday, as 'HH:MM' (24h) in the user's local timezone. Same pattern as the top-level field.",
256
+ "examples": ["17:00", "18:30"]
257
+ },
258
+ "hyperfocus_break_minutes": {
259
+ "type": "integer",
260
+ "minimum": 15,
261
+ "maximum": 240,
262
+ "description": "Override of 'chronometric.hyperfocus_break_minutes' for this weekday. Same 15..240 range as the top-level field; re-anchors the 'nudge' rung for this day only.",
263
+ "examples": [60, 120]
264
+ }
265
+ }
266
+ },
267
+ "protectedWindow": {
268
+ "type": "object",
269
+ "description": "A single local-time range where the hyperfocus monitor hard-surfaces. 'start' and 'end' are required 'HH:MM' (24h) strings; 'label' is an optional human-readable name. additionalProperties is false to catch typos in this small, fixed shape.",
270
+ "additionalProperties": false,
271
+ "required": ["start", "end"],
272
+ "properties": {
273
+ "start": {
274
+ "type": "string",
275
+ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
276
+ "description": "Window start, 'HH:MM' (24h), local timezone.",
277
+ "examples": ["12:00", "17:00"]
278
+ },
279
+ "end": {
280
+ "type": "string",
281
+ "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$",
282
+ "description": "Window end, 'HH:MM' (24h), local timezone. An 'end' earlier than 'start' is treated by the consumer as wrapping past midnight.",
283
+ "examples": ["12:30", "23:59"]
284
+ },
285
+ "label": {
286
+ "type": "string",
287
+ "minLength": 1,
288
+ "maxLength": 80,
289
+ "description": "Optional human-readable label for the window, surfaced in any notice the monitor emits.",
290
+ "examples": ["lunch", "evening", "lecture"]
291
+ }
292
+ }
293
+ }
294
+ },
179
295
  "examples": [
180
296
  {
181
297
  "identity": {