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

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 (101) hide show
  1. package/.dockerignore +3 -0
  2. package/.env.example +15 -0
  3. package/AGENTS.md +6 -3
  4. package/CHANGELOG.md +49 -0
  5. package/CONTRIBUTING.md +34 -4
  6. package/README.md +3 -0
  7. package/compose.yaml +8 -1
  8. package/docker/run.ts +1 -1
  9. package/docs/architecture/ai-selection.md +8 -0
  10. package/docs/architecture/authority-boundaries.md +24 -1
  11. package/docs/architecture/telegram-intake.md +1 -1
  12. package/docs/docker-runtime.md +35 -0
  13. package/docs/host-service.md +19 -0
  14. package/docs/pagerduty.md +42 -0
  15. package/docs/plugin-catalog.md +27 -10
  16. package/docs/plugin-contributions.md +9 -0
  17. package/docs/plugins.md +12 -1
  18. package/docs/releasing.md +20 -9
  19. package/docs/repair.md +41 -0
  20. package/docs/scheduling.md +30 -4
  21. package/docs/selective-monitoring.md +12 -4
  22. package/docs/setup.md +39 -0
  23. package/docs/trusted-publishing.md +140 -0
  24. package/docs/upgrades.md +24 -4
  25. package/package.json +6 -3
  26. package/scripts/generate-publish-caller.mjs +60 -0
  27. package/scripts/smoke-busy-reply.ts +58 -0
  28. package/scripts/trusted-beta.mjs +289 -0
  29. package/src/agent-guidance.ts +5 -0
  30. package/src/ai-cli.ts +2 -1
  31. package/src/ai.ts +15 -5
  32. package/src/client-defaults.ts +29 -13
  33. package/src/codex-session.ts +4 -2
  34. package/src/config.ts +29 -1
  35. package/src/control-state.ts +24 -7
  36. package/src/desktop-bridge.ts +8 -1
  37. package/src/event-sources.ts +2 -1
  38. package/src/execution-authority.ts +2 -1
  39. package/src/executor.ts +31 -6
  40. package/src/failure.ts +32 -0
  41. package/src/host-executor.ts +22 -13
  42. package/src/identity.ts +8 -3
  43. package/src/inbox.ts +7 -3
  44. package/src/index.ts +207 -79
  45. package/src/install-tools.mjs +2 -2
  46. package/src/menu.ts +6 -4
  47. package/src/model-policy.ts +15 -0
  48. package/src/owner.ts +3 -3
  49. package/src/pagerduty.ts +109 -0
  50. package/src/plugins/manager.mjs +47 -8
  51. package/src/plugins/shared.mjs +76 -0
  52. package/src/repair-policy.ts +13 -0
  53. package/src/reply-context.ts +67 -0
  54. package/src/reply-executor.ts +54 -0
  55. package/src/reply-mcp.ts +23 -0
  56. package/src/runs.ts +15 -4
  57. package/src/schedule-cli.ts +36 -7
  58. package/src/scheduler.ts +12 -3
  59. package/src/setup.ts +2 -1
  60. package/src/software-status.ts +5 -5
  61. package/src/task-cli.ts +3 -3
  62. package/src/task-executor.ts +7 -5
  63. package/src/tasks.ts +35 -17
  64. package/src/telegram-source.ts +94 -0
  65. package/src/updates/artifact.mjs +16 -0
  66. package/src/updates/binding.mjs +3 -1
  67. package/src/updates/control.mjs +4 -4
  68. package/src/updates/runtime.mjs +3 -1
  69. package/templates/agent/AGENTS.md +10 -2
  70. package/templates/agent/TOOLS.md +6 -0
  71. package/templates/agent-guidance.md +13 -0
  72. package/templates/failure-review.md +9 -0
  73. package/templates/maintainer-purpose.md +15 -0
  74. package/templates/updates.md +2 -2
  75. package/test/agent-guidance.test.ts +110 -0
  76. package/test/ai-cli.test.ts +7 -6
  77. package/test/ai.test.ts +41 -0
  78. package/test/busy-reply-relay.test.ts +41 -0
  79. package/test/client-defaults.test.ts +37 -5
  80. package/test/codex-context.test.ts +5 -2
  81. package/test/codex-session.test.ts +4 -2
  82. package/test/config.test.ts +29 -0
  83. package/test/executor.test.ts +11 -1
  84. package/test/failure.test.ts +250 -0
  85. package/test/group-owner.test.ts +36 -0
  86. package/test/host-executor.test.ts +38 -7
  87. package/test/intake-relay.test.ts +141 -4
  88. package/test/model-policy.test.ts +61 -0
  89. package/test/pagerduty.test.ts +104 -0
  90. package/test/plugin-manager.test.mjs +3 -2
  91. package/test/relay.test.ts +2 -2
  92. package/test/repair-policy.test.ts +23 -0
  93. package/test/reply.test.ts +131 -0
  94. package/test/schedule-cli.test.ts +8 -2
  95. package/test/shared-services.test.mjs +98 -0
  96. package/test/software-status.test.ts +5 -5
  97. package/test/task-native.test.ts +2 -2
  98. package/test/tasks.test.ts +14 -6
  99. package/test/telegram-source.test.ts +75 -0
  100. package/test/trusted-beta.test.mjs +224 -0
  101. package/test/updates.test.mjs +35 -3
@@ -32,6 +32,14 @@ the host changes zones. Nonexistent DST wall times are skipped; repeated wall
32
32
  times fire once, at the earlier instant. Search is bounded to eight years.
33
33
  Public-holiday calendars and arbitrary RRULE syntax are not implemented.
34
34
 
35
+ New tasks, including work deferred by a busy reply session, default to Codex
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
40
+ choice unless those flags override it. Stored choices are checked again at
41
+ launch, including schedules saved before the cap.
42
+
35
43
  ## Execution and authority
36
44
 
37
45
  The relay checks due work once per second. Each occurrence enters the durable
@@ -41,7 +49,7 @@ Instructions must include any needed context or source paths; full chat history
41
49
  is not copied. Task folders remain for inspection and artifact delivery.
42
50
 
43
51
  One writer runs per task directory. Up to four background tasks can run alongside
44
- the sequential main conversation. A recurring schedule has at most one pending
52
+ the main conversation. When a Codex owner message arrives while work is busy, a separate restricted session reads recent messages and run progress and answers through the normal outbox. It can queue requested work through the scheduler, but cannot run shell commands, access plugins, or edit the agent workspace. Only one reply session runs at a time and it releases its slot after a 60-second reply deadline; this deadline does not apply to writer jobs. Its context is a bounded snapshot, not a shared native transcript. Delivered parallel replies are included as historical context in the next normal conversation turn. Codex 0.153.4 and 0.154.0 are supported for this restricted adapter. Other versions fail closed pending tool-surface validation. A recurring schedule has at most one pending
45
53
  or active occurrence. Agents should delegate long work with `create --now`, return
46
54
  to chat, and inspect `runs` or task progress when asked. Native subagents can be
47
55
  used inside the worker. Sharing provider profiles does not make concurrent CRM,
@@ -70,8 +78,8 @@ The foreground chat still uses `codex exec`. That invocation exits after one
70
78
  requested turn even if a goal is active, so delegate persistent work to the
71
79
  scheduler. Desktop and other executor goal lifecycles need separate validation.
72
80
 
73
- The CLI binds jobs to the paired owner and current AI selection. Queued/scheduled
74
- work retains that selection after the chat switches AI. Revoking/re-pairing an
81
+ The CLI binds jobs to the paired owner and the task AI choice. Queued/scheduled
82
+ work retains that choice after the chat switches AI. Revoking/re-pairing an
75
83
  owner invalidates their old schedules, including re-pairing the same Telegram ID.
76
84
  External event turns cannot use the scheduling CLI. Credentials still pass only
77
85
  through the existing whitelist and installed host binding.
@@ -88,7 +96,7 @@ queued occurrence; already-running work continues until explicitly cancelled.
88
96
  `/stop` stops all active work; `cancel RUN_ID` stops one background task. `/cancel`
89
97
  clears queued work. Pause/remove a recurring schedule to prevent future runs.
90
98
  Stopping the relay also stops its workers. A crashed or interrupted execution is
91
- not automatically replayed. Runs found active at startup are marked failed with
99
+ not automatically replayed. Status labels failed runs as history and shows recent reasons; new failures retain their exit code or interruption cause. Typing indicators stop after 30 seconds even when work continues. Runs found active at startup are marked failed with
92
100
  `interrupted: true`; their schedule revision stays held until the agent inspects
93
101
  the evidence and explicitly edits the schedule. Inspect the task's files, native session and delivery
94
102
  receipts before deciding whether to resume. A clock cannot reconstruct an
@@ -125,3 +133,21 @@ minutes. Verify `finished.txt` and exactly one completion in Telegram. Separatel
125
133
  exercise cancellation, downtime catch-up and an explicitly requested native goal
126
134
  that needs more than one turn. Synthetic provider evidence does not prove real
127
135
  Telegram delivery, and a sleep test does not prove native goal persistence.
136
+
137
+ Busy-chat regression probe (real Codex, synthetic Telegram):
138
+
139
+ ```sh
140
+ pnpm exec tsx scripts/smoke-busy-reply.ts --transport
141
+ ```
142
+
143
+ The probe holds a writer on a shared workspace, asks an owner question through
144
+ the relay and host transport, and requires the restricted reply to complete
145
+ while the writer remains active. It sends no real Telegram messages.
146
+
147
+ ## Optional failure review
148
+
149
+ Create a normal recurring schedule with `--every-seconds 900 --when unreviewed-failures --text-file templates/failure-review.md`. The condition advances empty occurrences without launching an executor. It considers only failures belonging to the paired owner. No separate monitor or automatic retry is introduced.
150
+
151
+ `failures [--all] [--limit N]` returns failedAt, reason, exit code, native session, captured error and runtime versions. Capture keeps at most 4 KiB of redacted stderr; historical failures are not backfilled. `run RUN_ID` reads an owned run. `review RUN_ID --failed-at ISO --status resolved|attention --diagnosis TEXT --recovery TEXT --outcome TEXT` records the investigation without rewriting execution history. A stale timestamp is rejected; a later failure needs a new review. Restricted reply, external and isolated-task callers cannot review failures. An attention review is handed off, not repeatedly relaunched; another new failure wakes the next review.
152
+
153
+ The prompt controls diagnosis, authorized recovery and quiet notification behavior. Inspect prior effects and receipts before retrying anything. A failed review run itself remains visible as a new failure for the next occurrence.
@@ -33,7 +33,7 @@ Infer the complete job from ordinary language:
33
33
 
34
34
  Apply the required core confirmation to the concrete proposal, not an extra
35
35
  questionnaire. Use the owner's existing contact, purpose and disclosure limits.
36
- Avoid claiming indefinite service where v1 is bounded. Explain the expiry only
36
+ For an ongoing incoming-only conversation, use `--until-revoked`; finite tasks remain bounded. Explain the expiry only
37
37
  when it matters to that proposed job; never silently expand or renew permission.
38
38
 
39
39
  ## Three capture modes, separate reply authority
@@ -83,9 +83,17 @@ registered source, canonical contact, purpose, explicitly shareable context file
83
83
  and expiry. No need to invent a booking objective: “conversational replies to
84
84
  this contact, no private disclosures or commitments” is a legitimate purpose.
85
85
  If no private facts may be shared, say so in the context file; do not include
86
- owner memory. V1 supports at most 72 hours, not indefinite “until stopped.” Offer
87
- that bounded duration in the exact approval, explaining the limit without asking
88
- the owner to restate their request. Do not silently renew it.
86
+ owner memory. Finite jobs support at most 72 hours. For an ongoing conversation requested by
87
+ the owner, add `--incoming-only --until-revoked`. This authorizes that exact
88
+ conversation until owner revocation, without a total reply quota. Do not silently
89
+ convert a finite job into an ongoing permission.
90
+
91
+ For Telegram groups, the relay registers source `telegram` automatically when
92
+ paired. Use the exact negative group ID from discovery, never its display name.
93
+ Keep the shared context limited to what every group member may know. All human
94
+ members of that approved group may converse; this does not grant owner tools.
95
+ The group runs with the existing restricted messaging runner, and its notes are
96
+ separate from private PA memory. Group text is supported; media is not yet.
89
97
 
90
98
  The core presents the exact proposal for owner confirmation. Ordinary messages
91
99
  inside that grant need no repeated confirmations. Incoming-only grants create
package/docs/setup.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Agent-led setup
2
2
 
3
+ ## A group as owner
4
+
5
+ An installer can explicitly approve one Telegram group as the agent's owner.
6
+ Every human member in that group then has owner access, including settings,
7
+ approvals and scheduling. The conversation and replies are shared in that group.
8
+ New members inherit this access. Bots and anonymous sender-chat posts are ignored.
9
+
10
+ Add the bot to the intended group and send it a message addressed to its username.
11
+ `ezenciel-agents-owner status` shows the pending group title and negative chat ID;
12
+ verify the exact group with the installer, then run
13
+ `ezenciel-agents-owner approve-group <negative-chat-id>`. Pairing never happens
14
+ automatically. The existing `approve <user-id>` command approves DMs only.
15
+ An existing owner must be explicitly revoked before replacement; use a fresh
16
+ deployment when its existing workspace contains information unsuitable for the group.
17
+
18
+ Make the bot a group admin so Telegram delivers ordinary member messages and
19
+ allows membership verification for approval and control buttons. Group ownership only authorizes
20
+ the exact approved chat, not other groups or members' private DMs. Telegram group
21
+ migration to a new chat ID requires a new explicit binding. Verify a real group
22
+ reply before considering setup complete. All other groups retain their existing
23
+ restricted conversation-grant behavior.
24
+
3
25
  Ez supports two independent, composable uses: an autonomous Telegram agent and
4
26
  workspace-scoped plugins called by your existing local CLI/GUI executor.
5
27
  For CLI-only requests, follow [standalone setup](standalone-cli.md): no Telegram
@@ -30,6 +52,23 @@ owner request for standalone plugin development is a separate workflow.
30
52
 
31
53
  ## Defaults and host prerequisites
32
54
 
55
+ New agents using `codex` or `codex-gui` start with `gpt-5.6-terra` and `high`
56
+ reasoning, including when initialized with `ezenciel-agents-setup init`. This
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
61
+ Terra/high at launch. This governs Ez-managed launches; it is not an account-wide
62
+ limit on independently launched native clients or executor-created native subagents.
63
+ Other adapters inherit their native effort when none is selected in Ez. That
64
+ 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
+
67
+ New scheduled and one-off background tasks default to Codex Terra/high independently
68
+ of the creating chat. Use scheduler `--cli`, `--model`, and `--effort` flags for
69
+ explicit overrides. Editing a schedule preserves its settings unless overridden.
70
+ Restricted messaging tasks also use Terra/high when no settings are supplied.
71
+
33
72
  Use the existing owner's host account. Unless a layout was supplied, use
34
73
  `${XDG_DATA_HOME:-$HOME/.local/share}/ez/packages/<version>/` for extracted main
35
74
  packages and `${XDG_DATA_HOME:-$HOME/.local/share}/ez/agents/` for private deployments.
@@ -0,0 +1,140 @@
1
+ # Verified beta publication
2
+
3
+ The Mac prepares and independently tests the release. GitHub-hosted Actions
4
+ publishes the exact approved tarball using npm OIDC. The shared implementation
5
+ is `.github/workflows/npm-beta-shared.yml` in this repository; each package has a
6
+ small manually dispatched `publish-beta.yml` caller. This setup does not grant
7
+ release authority or establish npm trust automatically.
8
+
9
+ ## Enroll a repository once
10
+
11
+ 1. Verify the source is public and explicitly registered in the current
12
+ [public catalog](plugin-catalog.md), or is core itself. Registration does not
13
+ prove a registry release exists. Excluded or private repositories cannot use
14
+ this publisher.
15
+ 2. Review and merge the shared publisher first. Generate the caller using its
16
+ full immutable commit SHA and the repository's required CI check names:
17
+
18
+ ```sh
19
+ node scripts/generate-publish-caller.mjs jdorado/ez-whatsapp \
20
+ @jc_stack/ez-whatsapp FULL_SHARED_COMMIT_SHA \
21
+ '["test (ubuntu-latest, 22)","test (ubuntu-latest, 24)","test (macos-latest, 22)","test (macos-latest, 24)","docker"]' > publish-beta.yml
22
+ ```
23
+
24
+ Put that file in the plugin's `.github/workflows/` through its own reviewed
25
+ PR. Inspect the actual CI names; the example does not establish the policy.
26
+ Keep the reusable workflow reference and `publisher-sha` on the same reviewed
27
+ commit. Core uses a local reusable workflow at the dispatch commit. The
28
+ generator writes stdout only; it never grants repository scope or edits npm.
29
+ 3. The npm package owner authenticates separately and enrolls the exact caller
30
+ repository and filename `publish-beta.yml` as a trusted publisher. Enable
31
+ **direct publication** explicitly; new trust configurations can default to
32
+ staged publication only. If an environment is configured on npm, add that
33
+ exact environment to the shared publishing job through review before use.
34
+ This workflow currently uses no environment.
35
+ 4. Verify enrollment through npm settings or `npm trust list PACKAGE`. npm
36
+ validates the **calling** workflow for reusable workflows. Both caller and
37
+ publishing job need `id-token: write`; test/validation jobs do not receive it. GitHub requires `contents: write`
38
+ to read unpublished draft assets: only the validation job receives that
39
+ capability and makes GET requests only. The separate OIDC job has
40
+ `contents: read`. No package code or lifecycle scripts run in validation.
41
+ Do not add `NODE_AUTH_TOKEN`, npm tokens, or private profiles to these jobs.
42
+
43
+ An npm package must already exist before trust enrollment. If a registered
44
+ plugin has no registry package, the owner must perform a real, approved initial
45
+ beta publication using authenticated npm, with all release checks and exact
46
+ artifact readback. Then enroll trust. Do not create a dummy release to test
47
+ login. A missing GitHub release likewise remains missing until actually created
48
+ and read back; a catalog link or workflow PR is not publication evidence.
49
+
50
+ Current npm requirements and enrollment fields are documented in
51
+ [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/) and
52
+ [npm trust](https://docs.npmjs.com/cli/v11/commands/npm-trust/).
53
+ The workflow uses Node 24 and npm 11.19.1 on GitHub-hosted Ubuntu runners.
54
+
55
+ ## Prepare and dispatch one beta
56
+
57
+ Follow [releasing](releasing.md), including isolated Mac tests, packed artifact
58
+ inspection, independent review, required CI and authorized merge. The source
59
+ must be the exact current `main` commit; after a merge, verify that its tree
60
+ matches the tested source and renew invalidated evidence. Wait for that commit's
61
+ CI from `.github/workflows/ci.yml`, triggered by a push on `main`. The caller's reviewed list of required checks is a fail-closed minimum;
62
+ update it when repository policy adds checks. The validator selects the newest
63
+ main-push CI run for that source before checking success, then requires the named
64
+ jobs from its latest attempt. Tag, PR and other workflow runs cannot shadow it;
65
+ failed, pending or incomplete main CI cannot fall back to an older success.
66
+
67
+ Create the `vVERSION` tag at that exact source commit and a **draft prerelease**
68
+ with these assets, using native `gh release create --draft --prerelease` and
69
+ `gh release upload` under existing release authority:
70
+
71
+ - `candidate.tgz`: the exact Mac-tested bytes from `npm pack --ignore-scripts`.
72
+ Do not rebuild it on Actions.
73
+ - `release-receipt.json`: a sanitized record with this shape:
74
+
75
+ ```json
76
+ {
77
+ "repository": "jdorado/ez-agents",
78
+ "package": "@jc_stack/ez-agents",
79
+ "version": "0.1.0-beta.14",
80
+ "sourceSha": "FULL_TESTED_MAIN_COMMIT_SHA",
81
+ "sha256": "SHA256_OF_CANDIDATE_TGZ",
82
+ "independentReviewUrl": "https://github.com/jdorado/ez-agents/pull/PR_NUMBER#issuecomment-ID",
83
+ "testEvidenceUrls": ["https://github.com/jdorado/ez-agents/actions/runs/RUN_ID"]
84
+ }
85
+ ```
86
+
87
+ The maintainer verifies those evidence links substantiate independent final-diff
88
+ review, artifact tests and accepted beta limitations before dispatch. The receipt
89
+ binds that attestation to the commit and digest; a syntactically valid URL alone
90
+ cannot prove review quality or release authority.
91
+
92
+ Read the draft's numeric `id` with `gh api repos/OWNER/REPO/releases` (the
93
+ release-by-tag API only returns published releases). Dispatch `publish-beta.yml`
94
+ on `main` with `release-id`, `version`, `source-sha` and
95
+ `artifact-sha256`. Copy the digest from the independently verified Mac receipt,
96
+ not an unreviewed replacement release asset. The validator checks current public
97
+ scope, source/tag identity, required GitHub Actions checks, receipt identity,
98
+ package metadata and tarball hash. It transfers the validated bytes using an
99
+ immutable Actions artifact ID. A fresh job revalidates before publication and
100
+ runs npm from a clean directory without package lifecycle scripts.
101
+
102
+ Only `X.Y.Z-beta.N` versions are supported. Publish to npm `latest` so the
103
+ package page and default installs show the newest approved release. Manifests
104
+ must use `publishConfig.tag: "latest"` (or omit the tag). The version remains a
105
+ SemVer prerelease and the GitHub release remains a prerelease. Private
106
+ packages, wrong package/repository identities and stable versions fail before
107
+ publication. No npm login smoke publication or stable-version release occurs.
108
+
109
+ ## Readback, failure and release completion
110
+
111
+ The publisher reads registry metadata, checks that `latest` identifies the released version and downloads the
112
+ published tarball to compare its SHA-256. Preserve the workflow's readback receipt, run URL and source/artifact
113
+ identity on the release PR. A failed command after the publish call may mean npm
114
+ accepted it: inspect registry state first. A rerun may verify an existing exact
115
+ version; if the version is absent it refuses a second write. Reconcile first,
116
+ then create a fresh authorized dispatch if appropriate. Never repeat or overwrite that version or silently repair tags.
117
+ Missing trust or registry access is an external dependency, not a reason to use
118
+ a token workaround.
119
+
120
+ After successful registry readback, finish the GitHub prerelease with the tested
121
+ artifact/checksum and verify its public availability. Perform the clean-host
122
+ installation and runtime/provider checks required by the package's release
123
+ rules. Actions success proves registry delivery only; it does not prove a
124
+ running agent was upgraded. Respect each installation's saved update policy.
125
+
126
+ ## Migration from the legacy beta tag
127
+
128
+ Existing plugin callers are SHA-pinned: regenerate each caller against the merged
129
+ shared-publisher revision and update its package publishConfig together through
130
+ review. Old pins retain the old behavior. Do not mutate an already staged or
131
+ published artifact; prepare a new version when package metadata changes.
132
+
133
+ The publisher uses one native npm publish operation with OIDC and `--tag latest`.
134
+ It does not synchronize the legacy `beta` tag: npm trusted publishing does not
135
+ support standalone dist-tag changes. No extra registry token is needed. Ez beta
136
+ update discovery considers both latest and legacy beta during migration; stable-only
137
+ policies select non-deprecated stable versions and cannot automatically install a
138
+ prerelease. Older installed updaters still following only beta require an explicit
139
+ exact-version update to a core release containing this discovery change. Existing
140
+ registry versions/tags are not changed by merging the publisher.
package/docs/upgrades.md CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  Available in this beta. Earlier main upgrade/rollback VM QA passed; final-release
4
4
  fresh-host/reboot and live plugin upgrade acceptance remain pending. npm
5
- publication is not required to test this feature. Stable releases are the default
6
- automatic channel. The existing owner may select beta or manual policy per target.
5
+ publication is not required to test this feature. The beta channel is the default
6
+ automatic channel for core and plugins without a saved policy. Existing explicit
7
+ stable or manual policies are preserved. The owner may select either per target.
7
8
  The main target and installed plugins version independently.
8
9
 
9
10
  The agent owns release review, policy decisions and communication. The host
@@ -109,8 +110,8 @@ live. A healthy container alone does not prove a Telegram or plugin reply.
109
110
 
110
111
  ```sh
111
112
  ez updates check
112
- ez updates policy main # defaults: automatic, stable
113
- ez updates policy whatsapp beta # only under owner authorization
113
+ ez updates policy main # defaults: automatic, beta
114
+ ez updates policy whatsapp stable # opt into stable-only updates
114
115
  ez updates policy main manual # disable unattended upgrades
115
116
  ez updates prepare main --version 0.1.0-beta.4
116
117
  # Or a local candidate, independently of npm:
@@ -191,3 +192,22 @@ changes also need supervisor restart and requesting-process-exit tests. Run
191
192
  `node docker/upgrade-smoke.mjs` with a local `EZ_WHATSAPP_SOURCE` containing the
192
193
  WhatsApp fixture. The smoke uses synthetic transport only. VM installation,
193
194
  agent-led upgrades, restart and real account acceptance remain separate QA gates.
195
+
196
+ Beta policy discovers the newer of npm latest and the legacy beta tag. Stable-only
197
+ policy selects non-deprecated stable versions, even when latest is a prerelease.
198
+ No eligible version is reported as available:null with newer:false. Older installed
199
+ updaters need an exact-version core update to adopt this discovery behavior.
200
+
201
+ ## Shared agent guidance
202
+
203
+ Ez includes `templates/agent-guidance.md` from the running package in every
204
+ owner-worker prompt, including resumed CLI and desktop conversations and scheduled
205
+ owner work. After the runtime upgrades, the next turn receives the new guidance.
206
+ An already running turn keeps its original prompt. Restricted contact tasks and
207
+ reply-only workers retain their separate, bounded instructions.
208
+
209
+ Keep general operating defaults in this package-owned file. Keep agent purpose,
210
+ preferences and local conventions in the workspace's `AGENTS.md`, `SOUL.md`,
211
+ `USER.md` and memory files; upgrades preserve them. Shared guidance does not
212
+ grant permissions, and explicit owner instructions take precedence over its
213
+ defaults within existing execution permissions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jc_stack/ez-agents",
3
- "version": "0.1.0-beta.13",
3
+ "version": "0.1.0-beta.18",
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.",
@@ -47,11 +47,14 @@
47
47
  "test",
48
48
  "tsconfig.json",
49
49
  "scripts/assert-local-registry.mjs",
50
- ".dockerignore"
50
+ ".dockerignore",
51
+ "scripts/trusted-beta.mjs",
52
+ "scripts/generate-publish-caller.mjs",
53
+ "scripts/smoke-busy-reply.ts"
51
54
  ],
52
55
  "publishConfig": {
53
56
  "access": "public",
54
- "tag": "beta"
57
+ "tag": "latest"
55
58
  },
56
59
  "scripts": {
57
60
  "setup": "tsx --env-file-if-exists=.env src/setup.ts",
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // Print a reviewed caller. Does not write repositories or enroll npm trust.
3
+ import assert from 'node:assert/strict';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ export function generateCaller({ repository, packageName, publisherSha, checks }) {
7
+ assert.match(repository, /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/, 'Invalid repository');
8
+ assert.match(packageName, /^@[a-z0-9_-]+\/[a-z0-9_.-]+$/, 'Expected scoped npm package');
9
+ assert.match(publisherSha, /^[a-f0-9]{40}$/, 'Pin the reviewed shared publisher to a full commit SHA');
10
+ assert(Array.isArray(checks) && checks.length > 0 && checks.every(c => typeof c === 'string' && /^[A-Za-z0-9 (),_.-]+$/.test(c)), 'Required CI check names are mandatory');
11
+ const core = repository === 'jdorado/ez-agents';
12
+ const reference = core ? './.github/workflows/npm-beta-shared.yml' : `jdorado/ez-agents/.github/workflows/npm-beta-shared.yml@${publisherSha}`;
13
+ return `# Generated by scripts/generate-publish-caller.mjs; review changes before merging.
14
+ name: Publish verified npm beta
15
+ on:
16
+ workflow_dispatch:
17
+ inputs:
18
+ release-id:
19
+ description: 'Numeric ID of the staged draft prerelease'
20
+ required: true
21
+ type: string
22
+ version:
23
+ description: 'Reviewed beta version staged as draft release vVERSION'
24
+ required: true
25
+ type: string
26
+ source-sha:
27
+ description: 'Full tested commit SHA; must be current main'
28
+ required: true
29
+ type: string
30
+ artifact-sha256:
31
+ description: 'SHA-256 of the exact independently verified candidate.tgz'
32
+ required: true
33
+ type: string
34
+ permissions: {}
35
+ jobs:
36
+ publish:
37
+ if: github.repository == '${repository}' && github.ref == 'refs/heads/main'
38
+ permissions:
39
+ contents: write
40
+ checks: read
41
+ actions: read
42
+ pull-requests: read
43
+ id-token: write
44
+ uses: ${reference}
45
+ with:
46
+ package: '${packageName}'
47
+ publisher-sha: ${core ? '${{ github.sha }}' : `'${publisherSha}'`}
48
+ required-checks: '${JSON.stringify(checks)}'
49
+ release-id: \${{ inputs.release-id }}
50
+ version: \${{ inputs.version }}
51
+ source-sha: \${{ inputs.source-sha }}
52
+ artifact-sha256: \${{ inputs.artifact-sha256 }}
53
+ `;
54
+ }
55
+
56
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
57
+ const [repository, packageName, publisherSha, checksJson, ...extra] = process.argv.slice(2);
58
+ if (!checksJson || extra.length) throw new Error('Usage: node scripts/generate-publish-caller.mjs OWNER/REPO @SCOPE/PACKAGE SHARED_COMMIT_SHA \'["required CI name"]\'');
59
+ process.stdout.write(generateCaller({ repository, packageName, publisherSha, checks: JSON.parse(checksJson) }));
60
+ }
@@ -0,0 +1,58 @@
1
+ // Real restricted Codex reply while a synthetic writer stays active. No Telegram network.
2
+ import { mkdtemp, mkdir, writeFile, symlink } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import { tmpdir } from 'node:os'
5
+ import { spawn } from 'node:child_process'
6
+ import { createRelay } from '../src/index.js'
7
+ import { RunStore } from '../src/runs.js'
8
+ import { ControlStore } from '../src/control-state.js'
9
+ import { initialPreset } from '../src/ai.js'
10
+ import { startExecutorJob } from '../src/executor.js'
11
+ import { serveHostExecutor } from '../src/host-executor.js'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { initializeWorkspace } from '../src/workspace.js'
14
+ import type { Update } from 'grammy/types'
15
+ if (process.argv.includes('--host')) {
16
+ const root=process.argv[process.argv.indexOf('--host')+1], abort=new AbortController()
17
+ process.once('SIGTERM',()=>abort.abort())
18
+ await serveHostExecutor({cli:'codex',agents:[{name:'fixture',workspace:join(root,'mind'),controlDir:join(root,'control'),binDir:fileURLToPath(new URL('../bin',import.meta.url)),sharedWorkspace:join(root,'mind')}]},abort.signal,async(texts,options)=>{
19
+ if((await new RunStore(options.controlDir).get(options.runId))?.replyOnly)return startExecutorJob(texts,options)
20
+ const child=spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{detached:true,stdio:['pipe','pipe','pipe']})
21
+ return {child,stdout:'',cleanup:async()=>{}}
22
+ })
23
+ process.exit(0)
24
+ }
25
+ const root=await mkdtemp(join(tmpdir(),'ez-busy-reply-')), workspace=join(root,'mind'), controlDir=join(root,'control')
26
+ await initializeWorkspace(workspace);await mkdir(controlDir,{recursive:true})
27
+ if(process.env.EZ_REPLY_QA_AUTH){await mkdir(join(controlDir,'cli','codex'),{recursive:true});await symlink(process.env.EZ_REPLY_QA_AUTH,join(controlDir,'cli','codex','auth.json'))}
28
+ const hostMode=process.argv.includes('--transport')
29
+ const hostEnvironment={...process.env};delete hostEnvironment.EZ_EXECUTOR_TRANSPORT
30
+ const host=hostMode?spawn(process.execPath,['--import',fileURLToPath(new URL('../node_modules/tsx/dist/loader.mjs',import.meta.url)),fileURLToPath(import.meta.url),'--host',root],{env:hostEnvironment,stdio:['ignore','inherit','inherit']}):undefined
31
+ if(hostMode)process.env.EZ_EXECUTOR_TRANSPORT='host'
32
+ const control=new ControlStore(controlDir,1000),runs=new RunStore(controlDir)
33
+ await control.requestPairing(101,101);await control.approveOwner(101)
34
+ let writer:any,replyEvents=''
35
+ const relay=createRelay({workspace,controlDir,pairingTtlMs:1000,executorTimeoutMs:0,executorCli:'codex',telegramBotToken:'fixture'},async(texts,options)=>{
36
+ if(hostMode)return startExecutorJob(texts,options)
37
+ const run=await runs.get(options.runId)
38
+ if(run?.replyOnly){const job=await startExecutorJob(texts,options);job.child.stdout?.on('data',c=>{replyEvents+=c});return job}
39
+ const child=spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:['pipe','pipe','pipe']});writer=child
40
+ return {child,stdout:'',cleanup:async()=>{}}
41
+ })
42
+ relay.bot.botInfo={id:999,is_bot:true,first_name:'Fixture',username:'fixture_bot'} as any
43
+ const replies:string[]=[]
44
+ relay.bot.api.config.use(async(_p,method,payload)=>{if(method==='sendMessage')replies.push((payload as any).text);return {ok:true,result:method==='sendMessage'?{message_id:replies.length,date:0,chat:{id:101,type:'private'},text:(payload as any).text}:true} as any})
45
+ const msg=(id:number,text:string):Update=>({update_id:id,message:{message_id:id,date:0,text,from:{id:101,is_bot:false,first_name:'Fixture'},chat:{id:101,type:'private',first_name:'Fixture'}}})
46
+ try{
47
+ await relay.bot.handleUpdate(msg(1,'Long work'));await relay.drainInbox(true)
48
+ await relay.bot.handleUpdate(msg(2,'What is running? Also calculate 17 times 19. Use your available reply tools. Do not queue any work.'));await relay.drainInbox(true)
49
+ const started=Date.now()
50
+ while(!replies.length && Date.now()-started<120000){await relay.drainOutbox();await new Promise(r=>setTimeout(r,250))}
51
+ if(!replies.some(s=>s.includes('323')))throw new Error('No verified arithmetic reply: '+JSON.stringify(replies))
52
+ if((await runs.get('tg_1'))?.status!=='running')throw new Error('Writer stopped')
53
+ while((await runs.get('tg_2'))?.status==='running' && Date.now()-started<120000)await new Promise(r=>setTimeout(r,250))
54
+ if((await runs.get('tg_2'))?.status!=='completed')throw new Error('Reply did not finish successfully')
55
+ const reply=await runs.get('tg_2');if(!reply?.replyOnly)throw new Error('No restricted reply lane')
56
+ await writeFile(join(root,'evidence.json'),JSON.stringify({replyMs:Date.now()-started,replies,writerRunning:(await runs.get('tg_1'))?.status==='running',replyEvents},null,2))
57
+ console.log(JSON.stringify({root,replyMs:Date.now()-started,replies,writerRunning:true}))
58
+ }finally{await relay.stop();writer?.kill();host?.kill();delete process.env.EZ_EXECUTOR_TRANSPORT}