ennodia 0.2.0 → 0.3.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 (51) hide show
  1. package/README.md +76 -119
  2. package/docs/evidence/building-ennodia.md +66 -0
  3. package/docs/evidence/docs-drift.md +73 -0
  4. package/docs/evidence/index.md +18 -0
  5. package/docs/evidence/measurement.md +40 -0
  6. package/docs/evidence/skill-trial.md +68 -0
  7. package/docs/getting-started.md +55 -32
  8. package/docs/guides/agent-skills.md +22 -8
  9. package/docs/guides/budgets-and-limits.md +1 -1
  10. package/docs/guides/pragmatic-mode.md +122 -0
  11. package/docs/guides/recipes.md +56 -0
  12. package/docs/guides/troubleshooting.md +48 -0
  13. package/docs/guides/understand-results.md +41 -0
  14. package/docs/index.md +27 -90
  15. package/docs/install.md +49 -16
  16. package/docs/reference/controlled-english.md +2 -3
  17. package/docs/reference/ennodia-io.md +27 -4
  18. package/docs/reference/mcp-tools.md +167 -10
  19. package/docs/reference/supported-harnesses.md +40 -1
  20. package/docs/roadmap.md +25 -0
  21. package/examples/docs-drift/README.md +8 -0
  22. package/examples/docs-drift/archive/migration.md +4 -0
  23. package/examples/docs-drift/check.ts +7 -0
  24. package/examples/docs-drift/config.ts +8 -0
  25. package/examples/docs-drift/demo.ts +54 -0
  26. package/examples/docs-drift/recorded-run.json +16 -0
  27. package/examples/skill-trial/README.md +8 -0
  28. package/examples/skill-trial/check.ts +15 -0
  29. package/examples/skill-trial/contract.md +7 -0
  30. package/examples/skill-trial/recorded-trial.json +42 -0
  31. package/examples/skill-trial/retry.ts +4 -0
  32. package/package.json +5 -2
  33. package/skills/pragmatic/SKILL.md +40 -0
  34. package/src/advisor.ts +5 -0
  35. package/src/budget.ts +16 -10
  36. package/src/cli.ts +3 -0
  37. package/src/compare.ts +18 -12
  38. package/src/compositional.ts +22 -4
  39. package/src/core.ts +77 -47
  40. package/src/diagnosis.ts +44 -7
  41. package/src/harnesses.ts +286 -33
  42. package/src/history.ts +4 -1
  43. package/src/index.ts +3 -0
  44. package/src/plan-advice.ts +5 -2
  45. package/src/planner.ts +24 -1
  46. package/src/pragmatic.ts +52 -0
  47. package/src/process.ts +18 -0
  48. package/src/runs.ts +85 -10
  49. package/src/server.ts +47 -17
  50. package/src/tasks.ts +191 -25
  51. package/src/version.ts +1 -1
@@ -62,8 +62,25 @@ Environment variables:
62
62
  IO binds to loopback by default and refuses non-loopback hosts unless an API key
63
63
  is configured. Bearer tokens are compared with a timing-safe comparison.
64
64
 
65
+ IO checks the `Host` header on every route. In the default loopback posture,
66
+ the host must be `localhost`, `::1`, or an address in `127.0.0.0/8`. A forged
67
+ `Host` header, such as one from Domain Name System (DNS) rebinding, gets a
68
+ `403 invalid_host` error. An explicit non-loopback `--host` turns off this
69
+ allowlist and requires the API key.
70
+
71
+ IO checks the `Origin` header when a request carries one. Only loopback origins
72
+ are accepted. A browser post from a non-loopback origin gets a `403 invalid_origin`
73
+ error before any run starts. Clients that send no `Origin` header, such as
74
+ local command-line tools, keep working.
75
+
76
+ `POST /v1/chat/completions` requires a JSON media type such as
77
+ `application/json`. Other media types get a `415 unsupported_media_type` error.
78
+ This requirement stops simple-request browser posts, for example cross-site
79
+ forms that send `text/plain`.
80
+
65
81
  IO intentionally sends no Cross-Origin Resource Sharing (CORS) headers by
66
- default. The browser blocks local apps unless they use their own local proxy.
82
+ default. Browsers cannot read cross-origin responses or send cross-origin JSON posts
83
+ unless their app uses an appropriate same-origin proxy.
67
84
  There is no `--cors-origin` flag. Add one only when an app needs that trust
68
85
  boundary.
69
86
 
@@ -75,7 +92,7 @@ boundary.
75
92
  | `GET /v1/provider-options` | Supported | Returns app-facing local provider options for settings screens. |
76
93
  | `GET /v1/byok-options` | Supported alias | Same response as provider options. |
77
94
  | `GET /v1/models` | Supported | Returns local virtual model IDs for compatible clients. |
78
- | `POST /v1/chat/completions` | Supported subset | Non-streaming text messages only. |
95
+ | `POST /v1/chat/completions` | Supported subset | Non-streaming text messages only. Requires a JSON media type. |
79
96
 
80
97
  ## Provider Options
81
98
 
@@ -202,9 +219,15 @@ The response includes the normal OpenAI-style `choices[0].message.content` plus
202
219
  an `ennodia` object. The object contains the run ID, status, selected harnesses,
203
220
  task IDs, Compare ID when present, and budget estimate.
204
221
 
205
- Limits and failures are returned as JSON with `cache-control: no-store`:
222
+ Handler errors use JSON with `cache-control: no-store`. The native server can
223
+ reject a request before the handler runs, so clients must also handle an empty
224
+ HTTP error response:
206
225
 
207
- - request body above the configured cap -> `413 request_too_large`
226
+ - disallowed `Host` header or `Origin` header -> `403 invalid_host` or `403 invalid_origin`
227
+ - non-JSON media type on chat completions -> `415 unsupported_media_type`
228
+ - request body above the configured cap -> HTTP `413`. Bun can reject it with
229
+ an empty body before the handler runs. Requests rejected by the handler use
230
+ the JSON error `request_too_large`.
208
231
  - saturated chat completions -> `429 rate_limit_error` with `retry-after: 1`
209
232
  - client abort after the run starts -> `499 client_closed_request`. IO cancels
210
233
  the Ennodia run when Core exposes cancellation
@@ -40,6 +40,7 @@ as native Agent Skills: folders containing `SKILL.md`, installed in paths
40
40
  supported by each harness. It does not inline full skill content into the
41
41
  delegated prompt. Task and run views include selected skill metadata in
42
42
  `appliedSkills`.
43
+ This metadata records the request. It does not prove that the native agent loaded or followed the skill.
43
44
 
44
45
  `ennodia_estimate_budget`, `ennodia_estimate_compositional_budget`,
45
46
  `ennodia_start`, `ennodia_run`, `ennodia_start_compare`,
@@ -50,6 +51,10 @@ tokens, cache behavior, harness-internal context, or private subscription quota.
50
51
 
51
52
  ### Routing hints
52
53
 
54
+ Development versions of `ennodia_run` and `ennodia_start` accept optional `reasoningEffort` for Codex workers, forwarded through the native `model_reasoning_effort` configuration. Other adapters reject an explicit setting. Omission preserves native defaults. This field does not configure comparison agents.
55
+
56
+ Task and run views record the requested value. Model and CLI support remain authoritative.
57
+
53
58
  Ennodia uses `category` before keyword classification. Valid categories are
54
59
  `code`, `research`, `browser`, `image`, and `general`. The fallback classifier
55
60
  uses strong browser, image, code, and research signals. Bare words such as
@@ -75,13 +80,51 @@ Fields named `maxOutputChars`, `maxAnswerChars`, or `maxCandidateChars` bound
75
80
  returned text. `0` omits that text while still returning status, IDs, timing,
76
81
  and character counts.
77
82
 
83
+ For task views, `maxOutputChars` bounds each of `stdout`, `stderr`, and `finalMessage`.
84
+ `includeOutput: false` omits their text. `hasOutput` and `finalMessageChars` remain available in compact views.
85
+ `hasOutput` includes captured diagnostic output. It does not establish correctness or a useful answer.
86
+
87
+ On macOS and Linux, tasks own process groups. Cancellation sends TERM, then KILL after a bounded grace period.
88
+ Cleanup also removes descendants that remain in the owned group after the parent exits.
89
+ Processes that create a separate session can escape that group.
90
+ On Windows, cleanup signals the direct child process.
91
+ Closing the MCP input stream shuts down active work.
92
+
93
+ Raw and compositional batch starts cancel earlier workers if a later spawn fails.
94
+ Active runs retain child evidence until comparison and receipt capture finish.
95
+
96
+ ### Media review
97
+
98
+ Ennodia transports text prompts with local file paths. Native media inspection depends on the selected harness, model, available tools, and permissions.
99
+ Discovery and Plan Advisor expose optional, bounded `inputGuidance: string[]` for adapter observations and input advice.
100
+ This guidance does not guarantee successful ingestion or model access.
101
+
102
+ Media prompts also receive preparation guidance at worker launch, recorded by an `input-guidance` task event.
103
+ Budget estimates include this added prompt text.
104
+ Automatic native-audio routes suggest one worker without comparison. Keyword routing does not verify the worker's native access.
105
+ For Antigravity, see [supported harnesses](/docs/reference/supported-harnesses/#antigravity) for the text-only wire format and scoped native-media observations.
106
+
107
+ 1. Run a small native-media probe before launching a larger review or comparison.
108
+ 2. Include exact local files, requested ranges, and the reference in each worker prompt.
109
+ 3. Match excerpts, playback level, and reference material across audio candidates.
110
+ 4. Request the model, inspected files and ranges, native tool results, errors, and excerpt-specific findings.
111
+ 5. Retain task IDs and available terminal history receipts with the final assessment.
112
+
113
+ Successful media ingestion and excerpt-specific observations support a native listening claim.
114
+ Transcription, digital signal processing (DSP), and text-only judges provide separate evidence. Label each method and any fallback explicitly.
115
+ A Judge's comparison of written reports does not establish that the Judge inspected the source media.
116
+ Task output and history receipts are bounded. They do not provide complete transcripts or a permanent audit record.
117
+
78
118
  ## Discovery and planning
79
119
 
80
120
  ### `ennodia_list_harnesses`
81
121
 
82
122
  Detects supported local artificial intelligence (AI) tools and reports
83
123
  availability, runnable state, command path, version, capabilities, and adapter
84
- notes.
124
+ notes. Optional `inputGuidance` contains a bounded string array of input advice and scoped observations.
125
+
126
+ Version probes have bounded deadlines. A failed probe appears in adapter notes without blocking other discoveries.
127
+ `runnable` means Ennodia found an executable adapter. Authentication and requested model access still require a real task.
85
128
 
86
129
  | Input | Default | Meaning |
87
130
  | --- | --- | --- |
@@ -150,6 +193,10 @@ limits. It can propose only explicit worker slices containing a prompt, harness,
150
193
  optional model, and skill IDs. It cannot choose `cwd`, environment variables,
151
194
  arguments, permissions, isolation, timeouts, retries, or budgets.
152
195
 
196
+ Harness inventory entries preserve optional `inputGuidance: string[]` from discovery.
197
+ Plan Advisor treats this bounded guidance as observations, not proof of native media support.
198
+ Slices execute concurrently. For unverified native access, its instructions request a probe-only plan, followed by a new plan after the probe.
199
+
153
200
  The lifecycle is deliberately two-phase: request and inspect advice first, then
154
201
  launch the exact validated plan in a separate call. Ennodia rejects invalid
155
202
  Plan Advisor output as a whole and revalidates the plan digest and current inventory
@@ -332,12 +379,13 @@ Poll it with
332
379
 
333
380
  ### `ennodia_get_run`
334
381
 
335
- Returns the current run state.
382
+ Returns the current run state. Set `waitMs` to wait for completion without repeated immediate polls. A wait timeout leaves the run running.
336
383
 
337
384
  | Input | Default | Meaning |
338
385
  | --- | --- | --- |
339
386
  | `runId` | required | ID returned by `ennodia_run`. |
340
- | `includeEvents` | `true` | Include run event history. |
387
+ | `waitMs` | `0` | Wait up to 300000 milliseconds for a terminal state. Does not cancel or extend the run. Longer waits require a client tool timeout greater than this value. |
388
+ | `includeEvents` | `true` | Include run event history. Set false for compact waiting responses. |
341
389
  | `maxEvents` | `100` | Maximum run events to return. Capped at 300. |
342
390
  | `maxAnswerChars` | `80000` | Maximum final-answer characters. Capped at 200000. |
343
391
 
@@ -447,8 +495,8 @@ successful non-empty task IDs ready for a Judge + Result Advisor comparison.
447
495
  | `taskIds` | required | Shard task IDs returned by `ennodia_start_compositional`. |
448
496
  | `prompt` | none | Optional comparison prompt included in `compareNext` when the minimum outputs are ready. |
449
497
  | `minSuccessfulTasksForCompare` | `2` | Minimum successful non-empty outputs required for `compareReady`. |
450
- | `includeOutput` | `false` | Include bounded stdout and stderr previews for known tasks. |
451
- | `maxOutputChars` | `2000` | Maximum stdout and stderr characters per task when output is included. |
498
+ | `includeOutput` | `false` | Include bounded stdout, stderr, and finalMessage previews for known tasks. |
499
+ | `maxOutputChars` | `2000` | Maximum characters per task output field when output is included. |
452
500
 
453
501
  The response includes `readyTaskIds`, `runningTaskIds`, `failedTaskIds`,
454
502
  `cancelledTaskIds`, `emptySucceededTaskIds`, and `missingTaskIds`. It includes
@@ -462,13 +510,14 @@ Returns task status, captured output, events, timing, and ETA.
462
510
  | Input | Default | Meaning |
463
511
  | --- | --- | --- |
464
512
  | `taskId` | required | ID returned by a raw, compositional, advised-plan, run, Plan Advisor, or Compare start. |
465
- | `includeOutput` | `true` | Include bounded stdout and stderr. |
513
+ | `includeOutput` | `true` | Include bounded stdout, stderr, and finalMessage. |
466
514
  | `includeEvents` | `true` | Include bounded task events. |
467
- | `maxOutputChars` | `20000` | Maximum stdout and stderr characters. Capped at 200000. |
515
+ | `maxOutputChars` | `20000` | Maximum characters per output field. Capped at 200000. |
468
516
  | `maxEvents` | `100` | Maximum task events. Capped at 300. |
469
517
 
470
518
  A task is terminal only after the child process exits and stdout/stderr have
471
- drained or timed out visibly.
519
+ drained or timed out visibly. Final-message collection and adapter error checks complete before terminal status becomes visible.
520
+ Antigravity's explicit no-output error is a failure even when its command exits with code zero.
472
521
 
473
522
  ### `ennodia_cancel_task`
474
523
 
@@ -485,9 +534,9 @@ returns a compact view. Request output or events only when you need them.
485
534
 
486
535
  | Input | Default | Meaning |
487
536
  | --- | --- | --- |
488
- | `includeOutput` | `false` | Include bounded stdout and stderr previews for each task. |
537
+ | `includeOutput` | `false` | Include bounded stdout, stderr, and finalMessage previews for each task. |
489
538
  | `includeEvents` | `false` | Include bounded task events for each task. |
490
- | `maxOutputChars` | `4000` | Maximum stdout and stderr characters per task. Capped at 200000. |
539
+ | `maxOutputChars` | `4000` | Maximum characters per task output field. Capped at 200000. |
491
540
  | `maxEvents` | `25` | Maximum events per task. Capped at 300. |
492
541
 
493
542
  ## Judge + Result Advisor
@@ -557,3 +606,111 @@ server process.
557
606
  | `includeEvents` | `false` | Include bounded Judge and Result Advisor events. |
558
607
  | `maxCandidateChars` | `2000` | Maximum characters per candidate. Capped at 200000. |
559
608
  | `maxEvents` | `25` | Maximum events per comparison. Capped at 300. |
609
+
610
+ ## Pragmatic run settings
611
+
612
+ In version 0.3.0, `ennodia_run`, `ennodia_estimate_budget`, `ennodia_start_compositional` and `ennodia_estimate_compositional_budget` accept optional `pragmatic: {recipe, acceptanceCriteria}`. Model and harness overrides remain optional.
613
+ Recipes are `investigate` and `patch`. Both instruct workers to avoid file changes.
614
+
615
+ Pragmatic preserves routing and comparison choices for one or more independent workers. Plan Advisor can propose tailored model and skill assignments. Retries are not automatic. Native harness permissions still apply.
616
+
617
+ Run views include the requested `model` and `pragmatic` settings.
618
+ See [Pragmatic mode](/docs/guides/pragmatic-mode/) for examples, evidence retrieval, and measurement limits.
619
+
620
+ ### Execution deadlines
621
+
622
+ Task receipts include `deadlineAt` and `timeoutSource` (`caller` or
623
+ `task-manager-default`). The deadline is the process cutoff, not a prediction
624
+ of completion. Every child prompt receives its execution allowance and source.
625
+ It asks for useful partial findings before the cutoff and reports of
626
+ missing tools or permissions. Caller tools are not
627
+ automatically inherited. This notice does not grant access or extend deadlines.
628
+
629
+ A timed-out task is an interrupted attempt. Review its captured
630
+ output, events, partial changes, scope, and access before deciding how to proceed.
631
+ A timeout does not establish model inability.
632
+
633
+ Opt-in Codex continuation is described below.
634
+
635
+ Cancellation is visible immediately. In version 0.3.0, the history snapshot
636
+ is saved after child processes finish and their output drains. Use a bounded
637
+ `ennodia_get_run.waitMs` call when you need to wait for the saved evidence.
638
+ The wait limit does not stop the child process or extend its execution limit.
639
+
640
+ ### Persistent Codex workers
641
+
642
+ Set `persistSession: true` on `ennodia_run` to retain a native Codex session.
643
+ Use an explicit `harnessId: "codex"` and a persistent working directory.
644
+ Persistence cannot be combined with `isolateCwd`, parallel mode, or comparison.
645
+ The Codex adapter defaults to its read-only sandbox on each turn.
646
+
647
+ After the worker settles, inspect its task receipt. If `canContinue` is true,
648
+ pass that task ID as `continueTaskId` in the next run. The follow-up keeps the
649
+ harness and directory. It inherits the model and reasoning effort unless you
650
+ supply new values. Each follow-up has a new task ID and execution allowance.
651
+ An earlier turn cannot be used once a follow-up has started.
652
+
653
+ Continuation requires a task still held by the same running Ennodia server.
654
+ It does not import arbitrary native session IDs or restore ownership after
655
+ a server restart. Native session files remain under the harness's retention
656
+ policy. Ordinary runs remain ephemeral.
657
+
658
+ Preflight input estimates exclude the retained native context. Use each
659
+ turn's reported usage to assess cost. Persistence does not prove a cache hit
660
+ or a cost saving. An interrupted session may fail to resume, so preserve its
661
+ partial findings and files as evidence too.
662
+
663
+ ### Compact status reads
664
+
665
+ Set `compact: true` on `ennodia_get_run` to omit the routing plan, budget
666
+ explanation, prompt preview, and skill metadata from repeated status reads.
667
+ Status, timing, task IDs, answers, errors, and diagnoses remain available.
668
+ The existing answer and event limits still apply. Use `compact: false` for
669
+ the full receipt. The default remains false.
670
+
671
+ For pending runs, use `waitMs: 30000`, `compact: true`, and
672
+ `includeEvents: false` when the installed schema supports these options.
673
+ A compact response is not a summary of the worker's findings. Inspect the
674
+ answer and any partial task evidence before drawing conclusions.
675
+
676
+ ### Native subagent controls
677
+
678
+ `ennodia_run` and `ennodia_start` accept `nativeSubagents: "disabled"` for Codex
679
+ workers. Codex receives native feature flags disabling its built-in subagent
680
+ tools. Task receipts record the setting, and persistent follow-ups inherit it.
681
+ Other harnesses reject this setting before workers start. Omitting it preserves
682
+ native defaults. Judge and Advisor tasks do not receive this worker setting.
683
+
684
+ This controls built-in delegation tools. It does not prevent a shell command
685
+ from starting another program, grant file permissions, or account for nested
686
+ agent usage. Do not infer a complete cost total from a parent-only receipt.
687
+
688
+
689
+ ### Native worker permissions
690
+
691
+ Set `nativeSandbox: "workspace-write"` on a normal Codex worker run only
692
+ when its assignment permits edits. Use `nativeSandbox: "read-only"` for
693
+ investigation. Omission defaults to read-only on every turn, including
694
+ continuations of write-enabled sessions. Write access is not inherited.
695
+
696
+ The setting uses the supported Codex sandbox option. Native configuration
697
+ still defines sandbox behavior and writable locations. This option does
698
+ not grant caller tools, deployment access, or permission bypasses.
699
+ Other adapters reject explicit sandbox settings. Pragmatic recipes reject
700
+ workspace-write. Judge and Advisor tasks keep their existing defaults.
701
+
702
+ Task receipts record the selected sandbox. Run receipts record the requested
703
+ setting.
704
+
705
+ ### Evidence and operation limits in version 0.3.0
706
+
707
+ Compositional status separates lost stream capture (`captureTruncated`) from shortened output previews (`outputTruncated`).
708
+ Lost captures are listed in `truncatedTaskIds` and excluded from ready task IDs.
709
+ A compact preview can omit a complete retained answer without making the task unready.
710
+ Compare metadata identifies truncated evidence, including its prompt limit.
711
+ Preserve full patch artifacts and verify them before application.
712
+
713
+ `maxChildTasks` limits workers selected by the current operation.
714
+ A later Compare call and its Judge and Result Advisor have separate limits.
715
+ `includeCompareEstimate` includes an estimate, not a reserved spending balance.
716
+ Count these stages, retries and verification when reporting whole-task cost.
@@ -32,7 +32,27 @@ installed and runnable on the current machine.
32
32
  ```
33
33
 
34
34
  The response reports availability, runnable state, command path, version,
35
- capabilities, and adapter notes.
35
+ capabilities, adapter notes, and optional `inputGuidance`.
36
+
37
+ `inputGuidance` is a bounded `string[]` of adapter observations and input advice.
38
+ It does not guarantee media support, authentication, or access to a requested model.
39
+ Plan Advisor receives this guidance in its harness inventory.
40
+
41
+ Discovery checks installed commands, not active authentication or model access.
42
+ A stalled version probe stops at its deadline and adds an adapter note.
43
+ Use a small real task to check the selected agent before sending larger work.
44
+
45
+ ## Media Inputs
46
+
47
+ Ennodia sends a text prompt through the selected CLI. Include local file paths and requested ranges in that prompt.
48
+ The harness must read those files through its available tools and normal permissions.
49
+ Model application programming interface (API) capabilities do not establish support through a harness's input transport or file tools.
50
+ Run a small native-media probe before a larger review or comparison.
51
+
52
+ Record the files, ranges, model, tool results, errors, and task IDs.
53
+ For audio comparisons, match excerpts, playback level, and reference material across candidates.
54
+ Native audio findings need evidence of successful media ingestion and observations tied to the requested excerpt.
55
+ Transcription, digital signal processing (DSP), and text-only judging provide distinct evidence. Label each method explicitly.
36
56
 
37
57
  ## Claude Code Models
38
58
 
@@ -57,6 +77,25 @@ such as `opencode-go/kimi-k2.7-code`.
57
77
 
58
78
  ## Antigravity
59
79
 
80
+ An explicit `jetski: no output produced` error is treated as a failed task even if the command exits with code zero.
81
+ Check the requested tool access in normal Antigravity settings. A denied read does not measure the model or skill's quality.
82
+
83
+ Ennodia uses `agy --print` with a text prompt containing local paths.
84
+ Antigravity's documented stream input accepts text blocks only. It rejects media blocks.
85
+ Use native `view_file` inspection when the selected harness and model expose it.
86
+ Check the tool result before claiming native audio, image, video, or document inspection.
87
+ See [headless input](https://antigravity.google/docs/cli/headless/#send-a-prompt) and the [CLI changelog](https://antigravity.google/changelog).
88
+
89
+ A September 10, 2026 probe used `agy` 1.2.0 with Gemini 3.8 Flash Medium:
90
+
91
+ - An eight-second MP3 native probe succeeded.
92
+ - FLAC sent as `audio/x-flac` was rejected.
93
+ - One comparison with four WAV samples timed out. This outcome leaves WAV support unresolved.
94
+
95
+ These observations apply to that run configuration. They do not define a universal format blacklist.
96
+ The CLI changelog records audio attachment support and media-type normalization fixes.
97
+ Gemini API audio support uses a separate interface. Its [format list](https://ai.google.dev/gemini-api/docs/audio) does not guarantee `view_file` compatibility.
98
+
60
99
  Antigravity can fail setup when the `agy` CLI is not on `PATH`. Tell the user or
61
100
  primary agent to verify:
62
101
 
@@ -0,0 +1,25 @@
1
+ ---
2
+ title: Roadmap
3
+ description: Reduce the work of choosing, trying, and maintaining agent tools.
4
+ ---
5
+
6
+ # Make the next useful attempt easier
7
+
8
+ The roadmap follows the work a user wants to avoid.
9
+ Future items are proposals, not available behavior or release dates.
10
+
11
+ | Stage | User outcome | Product work |
12
+ | --- | --- | --- |
13
+ | Current | Try another agent or skill from the same conversation. | Discovery, focused tasks, explicit choices, optional team advice, Compare, and local history. |
14
+ | Next release candidate | Trust the reported outcome and recover from failed attempts. | Process cleanup, valid comparison evidence, safer local server requests, clearer errors, and task-based guides. |
15
+ | Next | Avoid repeating unsuccessful attempts. | Local outcome notes with task, model, version, skill, and relevant conditions. |
16
+ | Later | Spend less time inspecting tool configuration. | Supported version and capability checks, clearer execution settings, and useful progress checkpoints. |
17
+ | Later | Get useful suggestions for the next attempt. | Recommendations grounded in recorded outcomes, with reasons and uncertainty. |
18
+ | Later | Reduce routine tool maintenance. | Configured updates through supported interfaces, version pinning, and recovery. |
19
+
20
+ A failed login must not become a negative model rating.
21
+ One unsuccessful attempt must not permanently exclude a model or skill.
22
+ Recorded outcomes need a way to inspect, correct, and delete them.
23
+
24
+ The [measurement plan](/docs/evidence/measurement/) covers task correctness, time, resources, and human work.
25
+ General savings claims will need comparative evidence.
@@ -0,0 +1,8 @@
1
+ # Small Service
2
+
3
+ This is an intentionally inconsistent documentation fixture for Ennodia.
4
+
5
+ The current default port is 3000. Configure another port with `PORT`.
6
+ The readiness response is `{ "status": "ok" }`.
7
+
8
+ The [migration record](./archive/migration.md) describes the previous release.
@@ -0,0 +1,4 @@
1
+ # Migration record — previous release
2
+
3
+ The previous release used port 3000, the `PORT` variable, and `{ "status": "ok" }`.
4
+ This file is a historical record, not a current configuration guide.
@@ -0,0 +1,7 @@
1
+ import assert from "node:assert/strict";
2
+ import { DEFAULT_PORT, readPort, readiness } from "./config";
3
+ assert.equal(DEFAULT_PORT, 4545);
4
+ assert.equal(readPort({ APP_PORT: "5050" }), 5050);
5
+ assert.equal(readPort({ PORT: "5050" }), 4545);
6
+ assert.deepEqual(readiness(), { ready: true });
7
+ console.log("Current behavior verified. Compare README claims with config.ts; preserve the labeled migration record.");
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_PORT = 4545;
2
+ export function readPort(env: Record<string, string | undefined>): number {
3
+ const value = env.APP_PORT;
4
+ return value === undefined ? DEFAULT_PORT : Number(value);
5
+ }
6
+ export function readiness(): { ready: boolean } {
7
+ return { ready: true };
8
+ }
@@ -0,0 +1,54 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { resolve } from "node:path";
4
+
5
+ // Opt-in live example. This starts one real agent and uses its configured access.
6
+ if (!process.argv.includes("--live")) {
7
+ console.log("Run: bun examples/docs-drift/demo.ts --live <harness-id>");
8
+ console.log("Example: bun examples/docs-drift/demo.ts --live antigravity");
9
+ process.exit(0);
10
+ }
11
+ const harnessId = process.argv[process.argv.indexOf("--live") + 1];
12
+ if (!harnessId) throw new Error("Select an installed, authenticated harness ID.");
13
+ const root = resolve(import.meta.dir, "../..");
14
+ const client = new Client({ name: "ennodia-docs-demo", version: "1" });
15
+ const transport = new StdioClientTransport({
16
+ command: "bun", args: ["run", "src/cli.ts"], cwd: root, stderr: "pipe",
17
+ });
18
+ async function call(name: string, args: Record<string, unknown>) {
19
+ const result = await client.callTool({ name, arguments: args });
20
+ const text = (result.content as { type: string; text?: string }[])
21
+ .filter((item) => item.type === "text").map((item) => item.text).join("\n");
22
+ if (result.isError) throw new Error(text);
23
+ return JSON.parse(text);
24
+ }
25
+ try {
26
+ console.log("Ennodia live example: find stale documentation");
27
+ console.log(`Agent: ${harnessId}; configured default model; no model override.`);
28
+ console.log("Read-only task: check README against config.ts; preserve labeled history.");
29
+ await client.connect(transport);
30
+ const run = await call("ennodia_run", {
31
+ harnessId, mode: "single", compare: false, cwd: import.meta.dir, timeoutMs: 120000,
32
+ prompt: "Read only README.md, config.ts, and archive/migration.md. Compare current README claims with config.ts. Return each conflict and its correction in plain text, with relative file names only. Explain if the labeled historical migration record needs changes. Do not read other files or edit anything. Keep the response under 180 words.",
33
+ });
34
+ console.log(`Run: ${run.id}\nStatus: ${run.status}`);
35
+ const deadline = Date.now() + 135000;
36
+ for (;;) {
37
+ await Bun.sleep(2000);
38
+ const result = await call("ennodia_get_run", { runId: run.id, includeEvents: false, maxAnswerChars: 5000 });
39
+ if (["succeeded", "failed", "cancelled"].includes(result.status)) {
40
+ console.log(`Status: ${result.status}\nElapsed: ${result.elapsedMs} ms`);
41
+ console.log(result.finalAnswer || JSON.stringify(result.diagnosis ?? "No answer returned."));
42
+ console.log("Check each finding against config.ts before using it.");
43
+ console.log("This is one live attempt, without a speed or cost comparison.");
44
+ if (result.status !== "succeeded") process.exitCode = 1;
45
+ break;
46
+ }
47
+ if (Date.now() > deadline) {
48
+ await call("ennodia_cancel_run", { runId: run.id });
49
+ throw new Error("The bounded demo wait expired.");
50
+ }
51
+ }
52
+ } finally {
53
+ await client.close();
54
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "recordedAt": "2026-09-06",
3
+ "kind": "single development trial",
4
+ "harness": "antigravity",
5
+ "model": "Configured default; exact model not established",
6
+ "runId": "d3e36bf1-07dd-4912-af33-b5c904603b44",
7
+ "taskIds": [
8
+ "e822af92-4410-4ed8-9382-f62a6f36a505"
9
+ ],
10
+ "status": "succeeded",
11
+ "elapsedMs": 13272,
12
+ "output": "* **Default Port Conflict**: [README.md:L5](../docs-drift/README.md#L5) claims the default port is `3000`, but [config.ts:L1](../docs-drift/config.ts#L1) defines [`DEFAULT_PORT`](../docs-drift/config.ts#L1) as `4545`.\n* **Port Environment Variable Conflict**: [README.md:L5](../docs-drift/README.md#L5) instructs users to configure ports using `PORT`, whereas [`readPort`](../docs-drift/config.ts#L2-L5) in [config.ts:L3](../docs-drift/config.ts#L3) reads `APP_PORT`.\n* **Readiness Response Conflict**: [README.md:L6](../docs-drift/README.md#L6) states the readiness response is `{ \"status\": \"ok\" }`, but [`readiness`](../docs-drift/config.ts#L6-L8) in [config.ts:L6-L7](../docs-drift/config.ts#L6-L7) returns `{ ready: true }`.\n* **Archive Status**: [archive/migration.md](../docs-drift/archive/migration.md#L1-L5) does **not** need changes; as stated in [archive/migration.md:L3-L4](../docs-drift/archive/migration.md#L3-L4), it is explicitly labeled as a historical migration record of the previous release rather than active configuration documentation.",
13
+ "edits": "Local file URL prefixes normalized to fixture-relative paths.",
14
+ "verification": "Primary agent checked all three conflicts and the historical distinction against the fixture.",
15
+ "limitation": "Known-answer fixture. No control run or speed or cost advantage established."
16
+ }
@@ -0,0 +1,8 @@
1
+ # Try a review skill on one small function
2
+
3
+ Compare `retry.ts` with `contract.md`. The function has deliberate defects.
4
+ Run a plain review and a review with a native installed skill in separate tasks.
5
+ Use the same model, files, and request. Save both findings before opening `check.ts`.
6
+
7
+ The evaluator contains the expected cases. Keep it out of the review material.
8
+ This example demonstrates the workflow. It does not establish that a skill improves quality.
@@ -0,0 +1,15 @@
1
+ import { retryDelaySeconds } from "./retry";
2
+ const cases: [string | null, number][] = [
3
+ [null, 30], ["0", 0], ["12", 12], ["120", 120], ["", 30], [" ", 30],
4
+ ["1.5", 30], ["-1", 30], ["121", 30], ["1e2", 30], ["0x10", 30],
5
+ ];
6
+ let failures = 0;
7
+ for (const [input, expected] of cases) {
8
+ const actual = retryDelaySeconds(input);
9
+ if (actual !== expected) {
10
+ failures += 1;
11
+ console.log(`${JSON.stringify(input)}: expected ${expected}, received ${actual}`);
12
+ }
13
+ }
14
+ console.log(`${cases.length - failures}/${cases.length} contract cases passed.`);
15
+ process.exitCode = failures ? 1 : 0;
@@ -0,0 +1,7 @@
1
+ # Retry delay contract
2
+
3
+ - A missing value uses 30 seconds.
4
+ - A decimal integer from 0 through 120 specifies the delay in seconds.
5
+ - Zero is valid and means retry immediately.
6
+ - Empty values, whitespace, fractions, negative values, and values over 120 use 30 seconds.
7
+ - The function must not accept numeric notation such as `1e2` or `0x10`.
@@ -0,0 +1,42 @@
1
+ {
2
+ "recordedAt": "2026-09-06",
3
+ "kind": "inconclusive native skill trial",
4
+ "harness": "antigravity",
5
+ "model": "Configured default; exact model not established",
6
+ "plain": {
7
+ "taskId": "125ffd40-c224-4615-a441-f31154a26096",
8
+ "status": "succeeded",
9
+ "elapsedMs": 18023,
10
+ "requestedSkills": [],
11
+ "output": "Contract violations identified in [`retryDelaySeconds`](./retry.ts#L2) based on [contract.md](./contract.md):\n\n* **Zero input (`\"0\"`)**: Returns `30` instead of `0` because `Number(\"0\")` evaluates to `0`, which is falsy in `0 || 30`.\n* **Values exceeding upper bound (`\"121\"`)**: Returns `121` instead of `30` because there is no `> 120` boundary check.\n* **Negative values (`\"-1\"`)**: Returns `-1` instead of `30` because negative numbers are truthy and not checked against `< 0`.\n* **Fractions (`\"1.5\"`)**: Returns `1.5` instead of `30` because `Number(\"1.5\")` parses floats without integer validation.\n* **Scientific notation (`\"1e2\"`)**: Returns `100` instead of `30`, directly violating the rule disallowing numeric notation like `1e2`.\n* **Hexadecimal notation (`\"0x10\"`)**: Returns `16` instead of `30`, directly violating the rule disallowing numeric notation like `0x10`.\n* **Binary / octal notation (`\"0b10\"`)**: Returns `2` instead of `30` because `Number()` parses alternative radix notations instead of strictly decimal integers.\n"
12
+ },
13
+ "requestedSkill": {
14
+ "taskId": "233ba69d-b691-4e34-94db-c3fa03a759bb",
15
+ "reportedStatus": "succeeded",
16
+ "observedOutcome": "No answer; required read_file access denied",
17
+ "exitCode": 0,
18
+ "elapsedMs": 17290,
19
+ "requestedSkills": [
20
+ {
21
+ "id": "source-grounded-audit",
22
+ "name": "source-grounded-audit",
23
+ "version": "local",
24
+ "hash": "87366950aa303123d38f7d97cd6cc01a842ca7cc18ffc41f75653bec76fe9cfa",
25
+ "source": "project",
26
+ "harnessIds": [
27
+ "antigravity",
28
+ "claude-code",
29
+ "codex",
30
+ "installable",
31
+ "opencode"
32
+ ],
33
+ "native": true
34
+ }
35
+ ],
36
+ "stderrExcerpt": "jetski: no output produced — a tool required the \"read_file\" permission that headless mode cannot prompt for, so it was auto-denied.",
37
+ "output": ""
38
+ },
39
+ "edits": "Local file URL prefixes normalized; CLI remediation instructions omitted from the diagnostic excerpt.",
40
+ "verification": "Plain findings checked against the contract and evaluator. This denied attempt cannot measure skill effectiveness.",
41
+ "limitation": "Uncontrolled native skill inventory; loading not verified. The zero-exit false success is fixed in the release candidate."
42
+ }
@@ -0,0 +1,4 @@
1
+ // Deliberately incomplete implementation for a review exercise.
2
+ export function retryDelaySeconds(value: string | null): number {
3
+ return Number(value) || 30;
4
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ennodia",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "mcpName": "io.github.cherninlab/ennodia",
5
- "description": "Local MCP server for AI team advice, multi-agent review, Compare, and traceable receipts",
5
+ "description": "Try other agents, models, and skills from your familiar AI workflow through a local MCP server",
6
6
  "license": "MIT",
7
7
  "type": "module",
8
8
  "packageManager": "bun@1.3.14",
@@ -35,6 +35,7 @@
35
35
  "bench/bug-recall/scorer.ts",
36
36
  "bench/fixtures/**/*",
37
37
  "bench/results/.gitkeep",
38
+ "examples/**/*",
38
39
  "src/cli.ts",
39
40
  "src/advisor.ts",
40
41
  "src/budget.ts",
@@ -46,7 +47,9 @@
46
47
  "src/history.ts",
47
48
  "src/index.ts",
48
49
  "src/internal.ts",
50
+ "src/pragmatic.ts",
49
51
  "src/planner.ts",
52
+ "src/process.ts",
50
53
  "src/plan-advice.ts",
51
54
  "src/priority.ts",
52
55
  "src/runs.ts",