@jc_stack/ez-agents 0.1.0-beta.18 → 0.1.0-beta.19

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 (53) hide show
  1. package/.env.example +5 -0
  2. package/AGENTS.md +6 -0
  3. package/CHANGELOG.md +14 -0
  4. package/CONTRIBUTING.md +29 -3
  5. package/Dockerfile +6 -0
  6. package/README.md +8 -2
  7. package/bin/ezenciel-agents-watch.mjs +8 -0
  8. package/compose.workforce-watch.yaml +33 -0
  9. package/docs/architecture/ai-selection.md +14 -7
  10. package/docs/plugin-catalog.md +1 -0
  11. package/docs/plugins.md +34 -0
  12. package/docs/responsive-channels.md +57 -0
  13. package/docs/scheduling.md +6 -4
  14. package/docs/setup.md +10 -6
  15. package/docs/workforce-watch.md +101 -0
  16. package/package.json +4 -2
  17. package/src/agent-guidance.ts +4 -0
  18. package/src/ai.ts +14 -6
  19. package/src/control-state.ts +5 -3
  20. package/src/desktop-bridge.ts +4 -2
  21. package/src/executor.ts +4 -2
  22. package/src/host-executor-client.ts +7 -1
  23. package/src/index.ts +58 -18
  24. package/src/menu.ts +4 -4
  25. package/src/model-policy.ts +8 -5
  26. package/src/plugins/manager.mjs +70 -2
  27. package/src/reply-context.ts +7 -3
  28. package/src/reply-executor.ts +2 -1
  29. package/src/reply-mcp.ts +1 -1
  30. package/src/runs.ts +0 -13
  31. package/src/schedule-cli.ts +1 -1
  32. package/src/scheduled-tasks.ts +33 -0
  33. package/src/scheduler.ts +11 -2
  34. package/src/setup.ts +2 -2
  35. package/src/task-executor.ts +2 -1
  36. package/src/updates/runtime.mjs +2 -1
  37. package/src/workforce-watch-cli.ts +14 -0
  38. package/src/workforce-watch.ts +155 -0
  39. package/templates/agent-guidance.md +11 -0
  40. package/templates/chat-guidance.md +23 -0
  41. package/test/agent-guidance.test.ts +15 -0
  42. package/test/ai.test.ts +40 -1
  43. package/test/event-sources.test.ts +4 -0
  44. package/test/failure.test.ts +13 -7
  45. package/test/host-executor.test.ts +16 -0
  46. package/test/intake-relay.test.ts +4 -0
  47. package/test/model-policy.test.ts +9 -1
  48. package/test/plugin-manager.test.mjs +49 -0
  49. package/test/reply.test.ts +22 -0
  50. package/test/runs.test.ts +7 -0
  51. package/test/schedule-cli.test.ts +2 -0
  52. package/test/scheduled-tasks.test.ts +43 -0
  53. package/test/workforce-watch.test.ts +180 -0
package/.env.example CHANGED
@@ -39,3 +39,8 @@ EZ_REPAIR_ENABLED=true
39
39
  # EZ_PAGERDUTY_STOCKS_HEALTH_URL=http://10.97.0.1:3006/health/critical
40
40
  # EZ_PAGERDUTY_POLL_SECONDS=30
41
41
  # EZ_PAGERDUTY_FAILURE_THRESHOLD=3
42
+
43
+ # Workforce Watch runs separately. Its enrollment secret and Telegram token are
44
+ # monitor-only credentials; do not put them in an agent workspace.
45
+ # EZ_WATCH_ENROLL_TOKEN=
46
+ # EZ_WATCH_TELEGRAM_CHAT_ID=
package/AGENTS.md CHANGED
@@ -94,3 +94,9 @@ proactively request only missing merge/release authority, then ship and verify;
94
94
  do not leave the maintainer to discover ready drafts or operate the release.
95
95
 
96
96
  - In channel-backend mode, the application owns native sessions and actions. Forward normalized inputs with stable run IDs, recover only by idempotent backend submission, and deliver replies through the existing outbox. Never launch a fallback CLI or pass relay credentials into an executor.
97
+
98
+ A coding-task handoff must include the pushed commit and draft PR URL, checks,
99
+ independent-review status and remaining QA. If PR creation is blocked, state the
100
+ blocker and preserved commit. The merging agent owns the post-merge worktree
101
+ cleanup check and reports removal or the specific reason to retain it. Follow
102
+ CONTRIBUTING.md for squash-merge evidence, ignored files and active-use checks.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.19
4
+
5
+ - Preserve host-backed run completion and delivery evidence while the relay
6
+ finalizes state, rather than treating the host PID as a relay-local worker
7
+ process. Interrupted host transport and deliberate schedule cancellation retain
8
+ their distinct terminal states.
9
+ - Include the reviewed scheduled-task status, Workforce Watch, plugin-folder,
10
+ scheduler-model, incident-reopening, and catalog updates merged after beta.18.
11
+
12
+ ## 0.1.0-beta.18.1
13
+
14
+ - Add explicit read-only existing-folder bindings for plugin services, preserving bindings across compatible updates without copying source files.
15
+
16
+
3
17
  ## 0.1.0-beta.18
4
18
 
5
19
  - Await complete relay cleanup after fatal Telegram polling errors and preserve the primary failure.
package/CONTRIBUTING.md CHANGED
@@ -5,7 +5,8 @@ and the relevant CLI help first. Open an issue for substantial scope changes;
5
5
  small fixes need no proposal. Internal plans and private QA belong outside this
6
6
  repository. Public docs describe shipped behavior and explicit limitations.
7
7
 
8
- 1. Fork/branch from main and make one coherent change. Preserve unrelated work.
8
+ 1. Start one coherent change in a dedicated worktree from fetched origin/main,
9
+ following the isolated-work rules below. Preserve unrelated work.
9
10
  2. Install Node 22+ and pnpm 10.30.3. Run `pnpm install --frozen-lockfile`.
10
11
  3. Change the code, user instructions and focused tests together. Authority,
11
12
  paths, credentials, cancellation and uncertain writes need negative tests.
@@ -48,9 +49,17 @@ docs/plugin-contributions.md.
48
49
  create one worktree/PR per repository and link dependencies and merge order.
49
50
  Branch from an unmerged feature only when the dependency is intentional and
50
51
  documented; do not quietly include it in an unrelated PR.
51
- - Open a draft PR when the change is reviewable. Report its scope, exact commit,
52
- validation and remaining QA. Continue through the release handoff below;
52
+ - Push the task branch and open a draft PR as soon as the first coherent change
53
+ is reviewable, before reporting implementation complete. Do not leave completed
54
+ work only in a local branch. Continue through the release handoff below;
53
55
  implementation completion alone does not authorize publication.
56
+ - Every handoff names the repository, worktree, branch, exact commit, PR URL,
57
+ checks, independent-review status and remaining QA with its next action. If
58
+ pushing or PR creation is blocked, report the blocker and preserved local
59
+ commit; do not call the PR workflow complete. Resume the same task branch.
60
+ - Integration/release worktrees are task worktrees too. Give any unique fix its
61
+ own PR or include it in the explicitly scoped integration PR; never leave a
62
+ successful local integration as the only copy of a fix.
54
63
  - Before merge, obtain an independent human or agent review of the final diff.
55
64
  The implementer's self-check and passing CI are not independent review.
56
65
  Reviewers inspect correctness, architecture, state/permissions and negative
@@ -66,6 +75,16 @@ docs/plugin-contributions.md.
66
75
  inspect it for uncommitted/untracked files and local-only commits. Remove only
67
76
  the clean task worktree after valuable work is preserved; never force cleanup.
68
77
  Delete its branch only after confirming merge or authorized abandonment.
78
+ - The agent completing an authorized merge owns the cleanup check in that same
79
+ task. Fetch current main and read back the PR state and merged head; a clean
80
+ status or an ahead/behind count alone is not merge evidence. Squash/rebase
81
+ merges may require PR-head ancestry or patch-equivalence checks. Any commits
82
+ added after the reviewed PR head must be accounted for separately.
83
+ - Before removal, check ignored files as well as tracked/untracked files, and
84
+ confirm the path is not used by an active task, runtime or pending QA. Preserve
85
+ local state and evidence; do not use force removal or blanket pruning. Report
86
+ either the removed worktree or a concrete retention reason and next action.
87
+ Keep upgrade QA worktrees until their explicit acceptance or abandonment.
69
88
 
70
89
  ## Agent-owned release handoff
71
90
 
@@ -104,3 +123,10 @@ git worktree add -b feat/task-name /absolute/worktrees/task-name origin/main
104
123
  Beta publishing workflow changes follow [trusted publishing](docs/trusted-publishing.md).
105
124
  Validate wrong source, repository, package, version and artifact inputs with
106
125
  negative tests. Never dispatch publication to test authentication.
126
+
127
+ Finished work leaves zero task worktrees. Worktrees are temporary for active
128
+ implementation, PR review and specific unfinished acceptance steps. A merged
129
+ change, a completed release or a possible future follow-up is not a reason to
130
+ retain its checkout. Record deferred work in the PR/issue with its next action;
131
+ recreate a worktree from the preserved commit when that work actually resumes.
132
+ Every retained worktree must name an active task or a concrete unfinished gate.
package/Dockerfile CHANGED
@@ -14,3 +14,9 @@ ENV HOME=/state/home EZ_AGENT_WORKSPACE=/workspace EZ_CONTROL_DIR=/state/control
14
14
  WORKDIR /workspace
15
15
  ENTRYPOINT ["/app/docker/entrypoint.sh"]
16
16
  CMD ["start"]
17
+
18
+ FROM dependencies AS workforce-watch
19
+ RUN chmod +x bin/ezenciel-agents-watch.mjs && mkdir /state
20
+ ENV EZ_WATCH_STATE_DIR=/state
21
+ WORKDIR /app
22
+ CMD ["node", "--import", "tsx", "src/workforce-watch-cli.ts"]
package/README.md CHANGED
@@ -114,9 +114,15 @@ transport, with separate agent workspaces and sessions. See
114
114
  For optional PagerDuty paging of a critical Stocks outage, see
115
115
  [PagerDuty critical-outage paging](docs/pagerduty.md).
116
116
 
117
+ For independent agent check-ins and concise incident context, see
118
+ [Workforce Watch](docs/workforce-watch.md). It runs separately from relays and
119
+ does not receive their executor or provider credentials.
120
+
117
121
  Telegram `/status` shows the running relay and host versions plus installed
118
- plugin versions. The agent's `ez status` adds verified plugin runtime states and
119
- upgrade job receipts. See [status and upgrades](docs/upgrades.md).
122
+ plugin versions. Its read-only **Scheduled tasks** control lists the paired
123
+ owner's saved task titles, instructions, timing, state and next run; it never
124
+ creates, changes or runs a task. The agent's `ez status` adds verified plugin
125
+ runtime states and upgrade job receipts. See [status and upgrades](docs/upgrades.md).
120
126
 
121
127
  ## You are the installing agent
122
128
 
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ const here=dirname(fileURLToPath(import.meta.url)),require=createRequire(import.meta.url),tsx=require.resolve('tsx'),entry=join(here,'..','src','workforce-watch-cli.ts')
7
+ const child=spawn(process.execPath,['--import',tsx,entry],{stdio:'inherit'})
8
+ child.on('exit',(code,signal)=>{if(signal)process.kill(process.pid,signal);process.exit(code??1)})
@@ -0,0 +1,33 @@
1
+ services:
2
+ workforce-watch:
3
+ image: ${EZ_WORKFORCE_WATCH_IMAGE:-ezenciel-agents:local}
4
+ build:
5
+ context: .
6
+ target: workforce-watch
7
+ init: true
8
+ restart: unless-stopped
9
+ read_only: true
10
+ cap_drop: [ALL]
11
+ security_opt: [no-new-privileges:true]
12
+ environment:
13
+ EZ_WATCH_PORT: 8080
14
+ EZ_WATCH_ENROLL_TOKEN: ${EZ_WATCH_ENROLL_TOKEN:?Set a private enrollment token}
15
+ EZ_WATCH_TELEGRAM_CHAT_ID: ${EZ_WATCH_TELEGRAM_CHAT_ID:-}
16
+ TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
17
+ secrets: [workforce_watch_pagerduty]
18
+ volumes:
19
+ - workforce-watch-state:/state
20
+ tmpfs: [/tmp]
21
+ ports:
22
+ - ${EZ_WATCH_BIND_ADDRESS:-127.0.0.1}:${EZ_WATCH_PORT:-9919}:8080
23
+ healthcheck:
24
+ test: [CMD, node, -e, "fetch('http://127.0.0.1:8080/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
25
+ interval: 30s
26
+ timeout: 5s
27
+ retries: 3
28
+ start_period: 10s
29
+ volumes:
30
+ workforce-watch-state:
31
+ secrets:
32
+ workforce_watch_pagerduty:
33
+ file: ${EZ_WATCH_PAGERDUTY_SECRET_FILE:?Set EZ_WATCH_PAGERDUTY_SECRET_FILE to the protected workforce PagerDuty routing-key file}
@@ -36,10 +36,17 @@ workspace JSON settings; OpenCode reports resolved config. Unknown defaults and
36
36
  opaque wrappers remain explicitly “client default”. No credentials are stored,
37
37
  no inference runs, no new dependency, and no cross-CLI session transfer.
38
38
 
39
- `/status` resolves the configured model and reasoning effort for the current
40
- client-default seed from the agent's isolated Codex profile when that metadata
41
- is available. If the profile has no explicit model, it uses Codex's native
42
- default-model catalog for display. It displays that snapshot only; setup and
43
- refresh do not save the fallback, so the seed remains a native client default
44
- for later conversations. Desktop Codex remains its own opaque client default
45
- rather than inheriting CLI configuration.
39
+ New Codex CLI and desktop agents seed **Responsive chat** (`gpt-5.6-sol`,
40
+ `medium`). Durable work keeps its independent `gpt-5.6-terra` / `high` default;
41
+ workers can explicitly select another model and effort for the job. Native
42
+ choices for other executors remain unchanged. No fallback is selected when a
43
+ model is unavailable.
44
+
45
+ Upgrades add the responsive preset to the menu without changing an existing
46
+ selection, default, session, or queued run. Choose it through Choose AI for the
47
+ current conversation or Settings for future conversations. `/status` shows the
48
+ saved selection. Legacy client-default seeds still use native configuration
49
+ metadata for display only.
50
+
51
+ See [responsive channels](../responsive-channels.md) for the conversational
52
+ policy and the separate boundaries for plugin correspondence and app backends.
@@ -11,6 +11,7 @@ has been published. Install only released plugins needed for the owner's request
11
11
  | [Composio](https://github.com/jdorado/ez_composio) | Discover integrations and full native tool schemas, connect requested accounts and perform authorized actions through a private broker. Requires a Composio project key; individual apps may require OAuth consent. | `@jc_stack/ez-composio` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-composio?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez_composio/releases) |
12
12
  | [GitHub](https://github.com/jdorado/ez_github) | Create repositories, commit and push through native Git/gh with a private per-agent profile. Requires GitHub browser consent and Ez 0.1.0-beta.13 or newer. | `@jc_stack/ez-github` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-github?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez_github/releases) |
13
13
  | [Library](https://github.com/jdorado/ez-library) | Preserve agent files and attachments, extract PDF text, and retrieve notes with QMD. Optional GitHub/Drive/Dropbox persistence requires separate setup and authorization. | `@jc_stack/ez-library` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-library?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez-library/releases) |
14
+ | [Resend](https://github.com/jdorado/ez-resend) | Receive Resend email into private idempotent receipts for agent-owned routing. Requires a Resend API key and an already-configured receiving domain. | `@jc_stack/ez-resend` | [npm versions](https://www.npmjs.com/package/@jc_stack/ez-resend?activeTab=versions) · [GitHub releases](https://github.com/jdorado/ez-resend/releases) |
14
15
 
15
16
  Release links are live records, not a guarantee that every registered package
16
17
  has a published version. Before installing, read npm metadata and the matching
package/docs/plugins.md CHANGED
@@ -239,3 +239,37 @@ A shared entry declares `identity` (stable host resource ID), `buildTarget`, `me
239
239
  Docker's unique container name arbitrates concurrent first creation. Existing resources must match ownership and implementation labels. Labels assume a trusted Docker administrator; they are not credentials. Different Docker daemons are separate sharing domains. Stopping/uninstalling a plugin never stops or deletes the shared worker or model volume. Automatic shared-worker upgrades and garbage collection are not implemented; coordinate replacement explicitly after detaching all clients. Uninstall/reinstall resets client opt-in.
240
240
 
241
241
  Shared workers have a hard Docker CPU quota of half a core by default, across all attached agents combined. This leaves capacity even on a one-core host; it is not 50% of every core. The reviewed descriptor can set a different `cpus` value. Discovery refuses a worker whose actual CPU quota has drifted, rather than silently attaching to an unlimited process. Builds are not covered by the runtime quota.
242
+
243
+ ## Existing local folders
244
+
245
+ Use operator-owned read-only folder bindings when a plugin needs files that
246
+ already exist on the host. Keep indexes and writable metadata in the plugin's
247
+ normal volume. This uses Docker bind mounts; it copies no source bytes and
248
+ starts no provider sync. File edits remain with the host's existing tools.
249
+
250
+ Stop the plugin before changing a binding:
251
+
252
+ ```sh
253
+ ez plugins stop library
254
+ ez plugins folder-bind library --service library --source /absolute/existing/notes --target /state/libraries/notes/files
255
+ ez plugins folders library
256
+ ez plugins start library
257
+ ```
258
+
259
+ The target must be a child of a volume declared by that service, without
260
+ colliding with another mount. Sources must be real existing directories outside
261
+ the private tools directory. Missing sources fail instead of creating empty
262
+ folders. Bindings are operator configuration, separate from package snapshots;
263
+ compatible upgrades retain them and incompatible target changes fail validation.
264
+ Use `folder-unbind ID --service NAME --target PATH` while stopped to detach a
265
+ folder. Uninstall retains bindings and volumes. Unbinding reveals any underlying
266
+ volume contents; inspect those before restarting to avoid using stale files.
267
+ Never enable another sync writer for an already synchronized host folder.
268
+
269
+ For Library, create/select the library name first and retain its QMD state while
270
+ binding the host tree at that library's `files` directory. Keep provider bindings
271
+ disabled for that source. Enable the normal shared embedding worker through
272
+ `plugins shared-enable library embeddings`. Verify `library sources`, real search,
273
+ and original readback from the actual executor. Document any differences between
274
+ indexed snapshots and current originals; do not replace Library with private
275
+ QMD runtimes or edit installed Compose/package files to bypass missing support.
@@ -0,0 +1,57 @@
1
+ # Responsive channels
2
+
3
+ Ez treats conversational channels as places to answer, clarify and hand off
4
+ work. A simple question uses the current conversation; a small authorized action
5
+ uses its canonical receipt. Substantial work gets a durable task with enough
6
+ context to finish and verify the job. The agent returns to conversation after
7
+ the handoff is saved, instead of waiting for the worker. This is agent guidance,
8
+ not a keyword classifier, automatic acknowledgement, or latency guarantee.
9
+
10
+ New Codex agents use Sol / medium for chat. Scheduled work defaults independently
11
+ to Terra / high; the agent can choose another model and effort for complex work.
12
+ `ezenciel-agents-schedule create --now --text-file FILE --model MODEL --effort high`
13
+ uses the existing scheduler (include `--name` for a useful task label). Busy owner
14
+ reply sessions expose the same independent model/effort choice through `defer`.
15
+ Its retry returns the first saved schedule; changing arguments does not revise
16
+ an accepted job. `xhigh` is available only for Luna; other models remain capped at high.
17
+
18
+ A handoff includes the objective, relevant context and paths, constraints,
19
+ authorized actions, acceptance checks and delivery destination. Background
20
+ sessions own verification and final delivery, and may use native subagents.
21
+ One writer per workspace still applies. Shared external resources require
22
+ coordination even when task directories differ. Status must distinguish a saved
23
+ schedule from actual execution and a verified result from a process exit.
24
+
25
+ The package loads `templates/chat-guidance.md` at each turn for CLI, desktop,
26
+ busy owner replies and approved plugin messaging tasks. Upgrades refresh this
27
+ behavior without rewriting the agent's personal files. Existing model choices
28
+ remain pinned; an upgrade adds Responsive chat as an available selection.
29
+
30
+ ## Channel and authority boundaries
31
+
32
+ Telegram owner conversations can schedule work under the owner's authority.
33
+ The existing restricted busy-reply session keeps Codex chat available while a
34
+ writer is active. Other executors retain their existing concurrency behavior.
35
+
36
+ WhatsApp and other plugin contacts use the approved messaging task's isolated
37
+ context and tools. They receive the conversational guidance and Sol / medium
38
+ selection, but cannot invoke owner schedules, shell tools or native subagents.
39
+ They report work outside their capabilities to the owner; that report is not
40
+ an instruction or permission to execute. Full delegation from a plugin contact
41
+ needs an explicitly scoped worker capability and return route; this update does
42
+ not grant one. The plugin name alone never confers owner authority.
43
+
44
+ An application using `channelBackendUrl` owns its conversation, model and job
45
+ lifecycle. The relay does not inject prompts or override the app's model. Such
46
+ backends (including AI Fit) must adopt the same handoff policy in their own
47
+ runtime to benefit. Reuse their canonical job system; do not create a second
48
+ agent in the transport. Telegram polling and app-side queue waits still count
49
+ toward user-visible latency.
50
+
51
+ ## Verification
52
+
53
+ Tests cover independent worker settings, preserved selections across upgrades,
54
+ idempotent handoffs, invalid settings, revocation and restricted tool boundaries.
55
+ Existing scheduler/host tests cover a conversational reply while work remains
56
+ active. Measure time to the first useful reply and verified task completion
57
+ separately on the deployed provider before claiming a performance improvement.
@@ -34,11 +34,13 @@ Public-holiday calendars and arbitrary RRULE syntax are not implemented.
34
34
 
35
35
  New tasks, including work deferred by a busy reply session, default to Codex
36
36
  `gpt-5.6-terra` with `high` reasoning independently of the creating chat.
37
- Use `--cli`, `--model`, and `--effort` to specify another choice; reasoning
38
- explicitly selected above `high` is rejected for every model. Non-Codex adapters
39
- inherit native effort when unset. Editing preserves the existing AI
37
+ New chats use the separate Sol/medium preset. Busy reply `defer` accepts optional
38
+ `model` and `effort` fields; retries preserve the first saved task choice.
39
+ Use `--cli`, `--model`, and `--effort` to specify another choice. `xhigh` is
40
+ available only with Codex `gpt-5.6-luna`; every other model remains capped at
41
+ `high`. Non-Codex adapters inherit native effort when unset. Editing preserves the existing AI
40
42
  choice unless those flags override it. Stored choices are checked again at
41
- launch, including schedules saved before the cap.
43
+ launch, including schedules saved before a policy change.
42
44
 
43
45
  ## Execution and authority
44
46
 
package/docs/setup.md CHANGED
@@ -52,22 +52,26 @@ owner request for standalone plugin development is a separate workflow.
52
52
 
53
53
  ## Defaults and host prerequisites
54
54
 
55
- New agents using `codex` or `codex-gui` start with `gpt-5.6-terra` and `high`
55
+ New agents using `codex` or `codex-gui` start with `gpt-5.6-sol` and `medium`
56
56
  reasoning, including when initialized with `ezenciel-agents-setup init`. This
57
57
  Ez default takes precedence over discovered host client defaults. Saved agent
58
- selections at or below high are preserved; use the AI settings to choose another model or effort.
59
- Ez rejects explicit reasoning above `high` for every model at selection and execution,
60
- including old saved or queued choices. Unset Codex model/effort resolves to
58
+ selections permitted by the current policy are preserved; use the AI settings to choose another model or effort.
59
+ Ez accepts `xhigh` only for `gpt-5.6-luna`; every other model rejects explicit
60
+ reasoning above `high` at selection and execution, including old saved or queued choices.
61
+ Unset Codex model/effort resolves to
61
62
  Terra/high at launch. This governs Ez-managed launches; it is not an account-wide
62
63
  limit on independently launched native clients or executor-created native subagents.
63
64
  Other adapters inherit their native effort when none is selected in Ez. That
64
65
  inherited configuration is not capped by Ez; explicit above-high Ez selections
65
- are still rejected. Only Codex adapters receive the default `high` effort.
66
+ are rejected unless they are Codex Luna/xhigh. Conversation presets pin Sol/medium; the lower-level
67
+ Codex fallback remains Terra/high for work without an explicit choice.
66
68
 
67
69
  New scheduled and one-off background tasks default to Codex Terra/high independently
68
70
  of the creating chat. Use scheduler `--cli`, `--model`, and `--effort` flags for
69
71
  explicit overrides. Editing a schedule preserves its settings unless overridden.
70
- Restricted messaging tasks also use Terra/high when no settings are supplied.
72
+ Restricted messaging tasks use Sol/medium while retaining their approved tool
73
+ and context boundaries. Upgrades add Responsive chat to saved choices without
74
+ replacing the selected/default preset. See [responsive channels](responsive-channels.md).
71
75
 
72
76
  Use the existing owner's host account. Unless a layout was supplied, use
73
77
  `${XDG_DATA_HOME:-$HOME/.local/share}/ez/packages/<version>/` for extracted main
@@ -0,0 +1,101 @@
1
+ # Workforce Watch
2
+
3
+ Workforce Watch is a separate, small Docker service for missed agent check-ins
4
+ and explicit terminal failures. It is not an agent, scheduler, log store or
5
+ remote-execution channel. It retains only the five latest compact activity
6
+ records supplied by each enrolled worker.
7
+
8
+ Run it in a failure domain separate from the workers it watches. In particular,
9
+ it must not rely on the Stocks VM to report a Stocks VM outage, and a separate
10
+ host is required to detect a Mac-wide outage.
11
+
12
+ ## Start the monitor
13
+
14
+ Set private monitor-only values, never agent-workspace or source-control values:
15
+
16
+ ```text
17
+ EZ_WATCH_ENROLL_TOKEN=<private fleet enrollment secret>
18
+ EZ_WATCH_PAGERDUTY_SECRET_FILE=/absolute/path/to/a-0600-workforce-pagerduty-key
19
+ TELEGRAM_BOT_TOKEN=<alert bot token>
20
+ EZ_WATCH_TELEGRAM_CHAT_ID=<owner chat id>
21
+ ```
22
+
23
+ PagerDuty is the fleet incident owner. Workforce Watch sends a `trigger` for
24
+ each missed check-in or terminal failure, then a `resolve` with the stable
25
+ deduplication key `ez:workforce:<worker-id>` after sustained recovery. Use one
26
+ approved Events API v2 routing key for the fleet; it may be the existing
27
+ PagerDuty integration when that is the desired escalation policy. Never put it
28
+ in a worker's environment. The secret file contains only that key, is mode
29
+ `0600`, and is mounted as `/run/secrets/workforce_watch_pagerduty`; it is never
30
+ a Compose or container environment value. Telegram is optional, supplementary
31
+ owner visibility. PagerDuty is required: a Watch deployment without its
32
+ routing-key secret fails closed rather than silently running without paging.
33
+
34
+ The owner may explicitly authorize the CTO bot token temporarily. A dedicated
35
+ alert bot remains preferable because it preserves an independent delivery identity.
36
+
37
+ ```sh
38
+ docker compose -f compose.workforce-watch.yaml up -d --build
39
+ curl http://127.0.0.1:9919/healthz
40
+ ```
41
+
42
+ The default bind is loopback. Put private TLS networking or a private reverse
43
+ proxy in front of it before remote workers use it; do not expose enrollment or
44
+ check-in traffic publicly.
45
+
46
+ ## Enroll and check in
47
+
48
+ The worker selects its monitor through its configured URL. It submits the fleet
49
+ enrollment secret once; the response contains the worker-specific secret. Store
50
+ that response only in the worker's private service environment and discard it
51
+ from shell history and logs.
52
+
53
+ ```sh
54
+ curl --fail-with-body -X POST "$EZ_WATCH_URL/v1/enroll" \
55
+ -H "Authorization: Bearer $EZ_WATCH_ENROLL_TOKEN" \
56
+ -H 'content-type: application/json' \
57
+ --data '{"workerId":"stocks-production","checkInSeconds":300,"graceSeconds":180,"severity":"critical"}'
58
+ ```
59
+
60
+ The supervisor, not the LLM, posts check-ins using the returned worker secret:
61
+
62
+ ```sh
63
+ curl --fail-with-body -X POST "$EZ_WATCH_URL/v1/workers/stocks-production/check-in" \
64
+ -H "Authorization: Bearer $EZ_WATCH_WORKER_TOKEN" \
65
+ -H 'content-type: application/json' \
66
+ --data '{"status":"ok","activity":"strategy receipt delivered","runId":"daily-strategy-2026-09-11"}'
67
+ ```
68
+
69
+ An unrecoverable condition sends `{"status":"failed","terminal":true,...}`.
70
+ Optional `activity`, `error`, `runId`, and `logsHint` fields are size-limited and
71
+ appear in an alert. They must be public-safe operational context, never stdout,
72
+ prompts, holdings, credentials, or provider payloads.
73
+
74
+ ## Inspect
75
+
76
+ The enrollment token reads redacted state; worker secrets never appear:
77
+
78
+ ```sh
79
+ curl --fail-with-body "$EZ_WATCH_URL/v1/workers" \
80
+ -H "Authorization: Bearer $EZ_WATCH_ENROLL_TOKEN"
81
+ ```
82
+
83
+ For host replacement or a suspected worker-secret exposure, rotate that worker's
84
+ secret with the enrollment token. The old secret stops working immediately:
85
+
86
+ ```sh
87
+ curl --fail-with-body -X POST "$EZ_WATCH_URL/v1/workers/stocks-production/rotate" \
88
+ -H "Authorization: Bearer $EZ_WATCH_ENROLL_TOKEN"
89
+ ```
90
+
91
+ Store the returned worker secret in the replacement supervisor before stopping
92
+ the prior one.
93
+
94
+ The service triggers once for a missed deadline or terminal failure. It sends a
95
+ recovery notice only after two clean check-ins by default to prevent flapping.
96
+
97
+ If a worker fails again during a partially delivered recovery, channels that
98
+ already accepted recovery receive a fresh trigger. Channels still open retain
99
+ their delivery state, so they do not receive duplicate opening alerts. This
100
+ applies to terminal failures, failed check-ins, and missed check-ins, including
101
+ after a Watch restart.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jc_stack/ez-agents",
3
- "version": "0.1.0-beta.18",
3
+ "version": "0.1.0-beta.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A lightweight foundation for persistent business AI assistants using existing AI harnesses, workspaces and plugins.",
@@ -20,7 +20,8 @@
20
20
  "ezenciel-agents-create": "bin/ezenciel-agents-create",
21
21
  "ezenciel-agents-host": "bin/ezenciel-agents-host",
22
22
  "ezenciel-agents-tools": "bin/ezenciel-agents-tools.mjs",
23
- "ezenciel-agents-ai": "bin/ezenciel-agents-ai.mjs"
23
+ "ezenciel-agents-ai": "bin/ezenciel-agents-ai.mjs",
24
+ "ezenciel-agents-watch": "bin/ezenciel-agents-watch.mjs"
24
25
  },
25
26
  "files": [
26
27
  "default-plugins.json",
@@ -35,6 +36,7 @@
35
36
  "CONTRIBUTING.md",
36
37
  "Dockerfile",
37
38
  "compose.yaml",
39
+ "compose.workforce-watch.yaml",
38
40
  "docker",
39
41
  "compose.whatsapp.yaml",
40
42
  "scripts/smoke.ts",
@@ -3,3 +3,7 @@ import { readFileSync } from 'node:fs'
3
3
  // Resolve against the installed package, never the agent's editable workspace.
4
4
  export const agentGuidance = (): string =>
5
5
  readFileSync(new URL('../templates/agent-guidance.md', import.meta.url), 'utf8').trim()
6
+
7
+ // Channel behavior is shared without exposing owner workspace guidance to contacts.
8
+ export const chatGuidance = (): string =>
9
+ readFileSync(new URL('../templates/chat-guidance.md', import.meta.url), 'utf8').trim()
package/src/ai.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CODEX_DEFAULT_MODEL, DEFAULT_EFFORT, assertEffort, allowedEffort } from './model-policy.js'
1
+ import { CODEX_DEFAULT_MODEL, DEFAULT_EFFORT, CODEX_CHAT_MODEL, CHAT_EFFORT, assertEffort, allowedEffort } from './model-policy.js'
2
2
  import { access, readFile } from 'node:fs/promises'
3
3
  import { constants } from 'node:fs'
4
4
  import { homedir } from 'node:os'
@@ -39,6 +39,14 @@ export const initialPreset = (cli: string): AiPreset => {
39
39
  }
40
40
  }
41
41
 
42
+ // Conversation defaults are independent of durable work and explicit saved choices.
43
+ export const chatPreset = (cli: string): AiPreset => {
44
+ const preset = initialPreset(cli)
45
+ return ['codex', 'codex-gui'].includes(preset.cli)
46
+ ? { ...preset, id: 'chat-default', name: 'Responsive chat', model: CODEX_CHAT_MODEL, effort: CHAT_EFFORT }
47
+ : preset
48
+ }
49
+
42
50
  export const installed = async (cli: string): Promise<boolean> => {
43
51
  if (cli === 'codex-gui') return Boolean(await desktopCodexPath())
44
52
  for (const directory of (process.env.PATH || '').split(delimiter)) {
@@ -53,8 +61,8 @@ export const readModels = async (home = homedir(), available = installed, codexH
53
61
  const models: ModelChoice[] = []
54
62
  const record = (value: unknown): Record<string, unknown> =>
55
63
  value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
56
- const efforts = (value: unknown, key: string): string[] =>
57
- (Array.isArray(value) ? value : []).map((e: unknown) => record(e)[key]).filter(safe).filter(allowedEffort)
64
+ const efforts = (value: unknown, key: string, model?: string, cli?: string): string[] =>
65
+ (Array.isArray(value) ? value : []).map((e: unknown) => record(e)[key]).filter(safe).filter(effort => allowedEffort(effort, model, cli))
58
66
  const json = async (file: string) => {
59
67
  try { return record(JSON.parse(await readFile(file, 'utf8'))) } catch { return {} }
60
68
  }
@@ -64,7 +72,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
64
72
  const info = record(record(entry).info)
65
73
  if (info.hidden || !safe(info.id)) continue
66
74
  models.push({ cli: 'grok', model: info.id, name: String(info.name || info.id).slice(0, 80),
67
- efforts: efforts(info.reasoning_efforts, 'value') })
75
+ efforts: efforts(info.reasoning_efforts, 'value', info.id, 'grok') })
68
76
  }
69
77
  }
70
78
  if (await available('codex')) {
@@ -73,7 +81,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
73
81
  const info = record(entry)
74
82
  if (info.visibility !== 'list' || !safe(info.slug)) continue
75
83
  models.push({ cli: 'codex', model: info.slug, name: String(info.display_name || info.slug).slice(0, 80),
76
- efforts: efforts(info.supported_reasoning_levels, 'effort') })
84
+ efforts: efforts(info.supported_reasoning_levels, 'effort', info.slug, 'codex') })
77
85
  }
78
86
  }
79
87
  if (await available('codex-gui')) {
@@ -89,7 +97,7 @@ export const readModels = async (home = homedir(), available = installed, codexH
89
97
  }
90
98
 
91
99
  export const validateSelection = async (p: AiPreset, catalog: ModelChoice[], available = installed): Promise<void> => {
92
- assertEffort(p.effort)
100
+ assertEffort(p.effort, p.model, p.cli)
93
101
  if (!isPreset(p) || !(await available(p.cli))) throw new Error('This CLI is not installed.')
94
102
  if (!p.model && !p.effort && p.cli !== 'agy') return
95
103
  const model = catalog.find((m) => m.cli === p.cli && m.model === p.model)
@@ -279,6 +279,7 @@ export class ControlStore {
279
279
  const preserved = ai.presets.filter((p) => !p.id.startsWith('detected_') ||
280
280
  p.id === ai.selectedId || p.id === ai.defaultId)
281
281
  ai.presets = [...preserved, ...discovered.filter((p) => !preserved.some((old) => old.id === p.id))]
282
+ if (initial.id === 'chat-default' && !ai.presets.some(p => p.id === initial.id)) ai.presets.push(initial)
282
283
  await this.writeState(state)
283
284
  })
284
285
  }
@@ -324,7 +325,7 @@ export class ControlStore {
324
325
 
325
326
  async savePreset(preset: AiPreset): Promise<void> {
326
327
  if (!isPreset(preset)) throw new Error('Invalid AI preset')
327
- assertEffort(preset.effort)
328
+ assertEffort(preset.effort, preset.model, preset.cli)
328
329
  await this.withLock(async () => {
329
330
  const state = await this.readState()
330
331
  if (!state.ai) throw new Error('AI settings not initialized')
@@ -341,7 +342,7 @@ export class ControlStore {
341
342
  const ai = state.ai
342
343
  const preset = ai?.presets.find((p) => p.id === id)
343
344
  if (!ai || !preset) throw new Error('Saved AI no longer exists')
344
- assertEffort(preset.effort)
345
+ assertEffort(preset.effort, preset.model, preset.cli)
345
346
  if ((state.activeSession?.sessionId ?? null) !== expectedSession) throw new Error('Menu expired. Open Choose AI again.')
346
347
  const current = ai.presets.find((p) => p.id === ai.selectedId)!
347
348
  if (state.activeSession && (current.cli !== preset.cli || !state.activeSession.cli) && !fresh) return false
@@ -359,7 +360,8 @@ export class ControlStore {
359
360
  await this.withLock(async () => {
360
361
  const state = await this.readState()
361
362
  if (!state.ai?.presets.some((p) => p.id === id)) throw new Error('Unknown AI preset')
362
- assertEffort(state.ai.presets.find(p => p.id === id)!.effort)
363
+ const preset = state.ai.presets.find(p => p.id === id)!
364
+ assertEffort(preset.effort, preset.model, preset.cli)
363
365
  state.ai.defaultId = id
364
366
  await this.writeState(state)
365
367
  })
@@ -1,4 +1,4 @@
1
- import { agentGuidance } from './agent-guidance.js'
1
+ import { agentGuidance, chatGuidance } from './agent-guidance.js'
2
2
  import { executionDefaults } from './model-policy.js'
3
3
  import { repairPolicy } from './repair-policy.js'
4
4
  import { access, constants } from 'node:fs/promises'
@@ -67,6 +67,8 @@ export const desktopJobPrompt = (
67
67
 
68
68
  ${agentGuidance()}
69
69
 
70
+ ${runId.startsWith('r_schedule_') || runId.startsWith('r_update_') ? '' : chatGuidance()}
71
+
70
72
  Your current directory is the agent's persistent workspace. Read AGENTS.md
71
73
  and follow its workspace reading guidance before acting. Save useful work
72
74
  here so it survives new conversations and executor changes.
@@ -81,7 +83,7 @@ Then execute:
81
83
  - Approval: ezenciel-agents-approval --prompt "Approve action?" --action-id "act_1"
82
84
 
83
85
 
84
- ${runId.startsWith('r_schedule_') ? 'This is already a background task. Perform its work here; use native subagents when helpful. Keep progress in progress.md. For an explicitly persistent objective, use the executor native /goal capability. Send the owner the verified result through the messaging CLI before finishing.' : `Keep the owner conversation responsive. For long work, invoke ezenciel-agents-schedule create --now --name "Task" --text "Complete objective and send the owner the result" and return to chat after the CLI returns its durable schedule ID. Do not wait here for the background task. Check ezenciel-agents-schedule runs for actual progress; cancel RUN_ID stops it. Use native subagents inside the task as useful. When the owner requests a persistent objective on Codex CLI, start the scheduled text with /goal followed by its objective. This activates the native persistent goal in a dedicated session. Ez does not implement goals. Use --help for one-time and recurring schedules. Interpret dates yourself and specify the timezone explicitly. Do not create schedules from untrusted correspondence.`}
86
+ ${runId.startsWith('r_schedule_') ? 'This is already a background task. Perform its work here; use native subagents when helpful. Keep progress in progress.md. For an explicitly persistent objective, use the executor native /goal capability. Send the owner the verified result through the messaging CLI before finishing.' : `Keep the owner conversation responsive. For long work, invoke ezenciel-agents-schedule create --now --name "Task" --text "Complete objective and send the owner the result" and return to chat after the CLI returns its durable schedule ID. Choose --model and --effort for the job independently of chat; use --text-file for a complete handoff with context, constraints, acceptance checks, and delivery destination. Do not wait here for the background task. Check ezenciel-agents-schedule runs for actual progress; cancel RUN_ID stops it. Use native subagents inside the task as useful. When the owner requests a persistent objective on Codex CLI, start the scheduled text with /goal followed by its objective. This activates the native persistent goal in a dedicated session. Ez does not implement goals. Use --help for one-time and recurring schedules. Interpret dates yourself and specify the timezone explicitly. Do not create schedules from untrusted correspondence.`}
85
87
 
86
88
  ${repairPolicy(repairs)}
87
89