@agentpulselabs/cli-plugin-pulse 0.2.0 → 1.0.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 (69) hide show
  1. package/README.md +69 -32
  2. package/package.json +10 -18
  3. package/src/commands/pulse/doctor.js +14 -0
  4. package/src/commands/pulse/hub-write.js +26 -0
  5. package/src/commands/pulse/install.js +14 -0
  6. package/src/commands/pulse/login.js +20 -0
  7. package/src/commands/pulse/mirror.js +27 -0
  8. package/src/commands/pulse/pair.js +38 -0
  9. package/src/commands/pulse/restart.js +14 -0
  10. package/src/commands/pulse/start.js +6 -18
  11. package/src/commands/pulse/status.js +6 -6
  12. package/src/commands/pulse/stop.js +24 -0
  13. package/src/daemon/main.js +36 -0
  14. package/src/lib/BaseCommand.js +23 -23
  15. package/src/lib/agentSessions.js +279 -0
  16. package/src/lib/application.js +656 -0
  17. package/src/lib/backoff.js +8 -0
  18. package/src/lib/budgets.js +85 -0
  19. package/src/lib/cliDefaults.js +20 -0
  20. package/src/lib/cloudAuth.js +189 -0
  21. package/src/lib/contract.sha256 +2 -0
  22. package/src/lib/daemonComposition.js +338 -0
  23. package/src/lib/daemonLock.js +55 -0
  24. package/src/lib/daemonLoop.js +69 -0
  25. package/src/lib/daemonProcess.js +366 -0
  26. package/src/lib/daemonRuntime.js +820 -0
  27. package/src/lib/drivers.js +280 -0
  28. package/src/lib/errors.js +19 -0
  29. package/src/lib/graphWindows.js +149 -0
  30. package/src/lib/http.js +84 -0
  31. package/src/lib/hubActions.js +74 -0
  32. package/src/lib/identity.js +102 -0
  33. package/src/lib/intents.js +590 -0
  34. package/src/lib/ledger.js +535 -0
  35. package/src/lib/lifecycle.js +123 -0
  36. package/src/lib/localHub.js +217 -0
  37. package/src/lib/localHubWriter.js +69 -0
  38. package/src/lib/mirror-contract.json +4299 -0
  39. package/src/lib/mirrorContract.js +513 -0
  40. package/src/lib/mirrorDaemon.js +127 -0
  41. package/src/lib/mirrorService.js +358 -0
  42. package/src/lib/mirrorSource.js +455 -0
  43. package/src/lib/mirrorSync.js +353 -0
  44. package/src/lib/oauthLogin.js +132 -0
  45. package/src/lib/ownerLock.js +99 -0
  46. package/src/lib/pairedConfig.js +40 -0
  47. package/src/lib/pairingLifecycle.js +179 -0
  48. package/src/lib/pathRedaction.js +89 -0
  49. package/src/lib/planContract.js +141 -0
  50. package/src/lib/priorityLane.js +80 -0
  51. package/src/lib/processOwnership.js +252 -0
  52. package/src/lib/processSupervisor.js +376 -0
  53. package/src/lib/redaction.js +128 -0
  54. package/src/lib/rootPolicy.js +372 -0
  55. package/src/lib/rulePacks.js +162 -0
  56. package/src/lib/secretStore.js +169 -0
  57. package/src/lib/selfRestart.js +261 -0
  58. package/src/lib/serviceManager.js +358 -0
  59. package/src/lib/storage.js +131 -0
  60. package/src/lib/subprocess.js +45 -0
  61. package/src/lib/tokenStore.js +97 -0
  62. package/src/lib/transport.js +286 -0
  63. package/src/lib/transportContract.js +37 -0
  64. package/LICENSE +0 -21
  65. package/src/commands/pulse/gate.js +0 -25
  66. package/src/commands/pulse/next.js +0 -14
  67. package/src/commands/pulse/submit.js +0 -21
  68. package/src/commands/pulse/verify.js +0 -21
  69. package/src/runPulse.js +0 -58
package/README.md CHANGED
@@ -1,48 +1,85 @@
1
- # @agentpulselabs/cli-plugin-pulse
1
+ # Pulse connector plugin
2
2
 
3
- An [oclif](https://oclif.io) plugin that adds the **`pulse`** topic — a portable, contract-enforced build
4
- discipline — to the `agentpulse` CLI. It is a thin `argv → core → stdout` seam over
5
- [`@agentpulselabs/pulse`](https://github.com/ashishcloud/agentpulse-pulse); all discipline lives in the core.
3
+ `@agentpulselabs/cli-plugin-pulse` contributes the `agentpulse pulse` command group.
6
4
 
7
- > **Published to public npm** under the `@agentpulselabs` org (`publishConfig.access: "public"`). The CLI
8
- > (`@agentpulselabs/cli`) supports plugins via `@oclif/plugin-plugins`; this plugin installs through it.
5
+ Commands:
9
6
 
10
- ## Install
7
+ ```text
8
+ agentpulse pulse install
9
+ agentpulse pulse pair [--org <slug>] [--platform <url>] --root <rootId>=<path>
10
+ agentpulse pulse login [--platform <url>]
11
+ agentpulse pulse start
12
+ agentpulse pulse status
13
+ agentpulse pulse doctor
14
+ agentpulse pulse stop
15
+ agentpulse pulse restart
16
+ agentpulse pulse mirror on|off|status
17
+ agentpulse pulse hub-write on|off|status
18
+ ```
19
+
20
+ ## Replaces the deprecated build-discipline Pulse plugin
21
+
22
+ Version 1.0.0 takes over the package name and the `pulse` topic from the deprecated build-discipline plugin (`@agentpulselabs/cli-plugin-pulse` 0.2.0, with `pulse gate|next|start|status|submit|verify`). Those commands are gone. The connector was previously developed as `@agentpulselabs/cli-plugin-hub`.
23
+
24
+ Remove the old plugin first for a clean replace, then install or link the connector. Always use the full scoped name; a bare `pulse` resolves to an unrelated npm package.
11
25
 
12
26
  ```bash
13
- npm i -g @agentpulselabs/cli # the CLI (if not already installed)
14
- agentpulse plugins install @agentpulselabs/cli-plugin-pulse
15
- agentpulse pulse --help
27
+ agentpulse plugins uninstall @agentpulselabs/cli-plugin-pulse
28
+ agentpulse plugins install @agentpulselabs/cli-plugin-pulse # once published
29
+ agentpulse plugins link <path-to-this-repo> # local development instead of install
16
30
  ```
17
31
 
18
- This is the **user-installed** integration path (design decision): opt-in, decoupled release cadence, and the
19
- same mechanism any third party uses to add `agentpulse` commands.
32
+ `agentpulse plugins update` does not move you to 1.0.0, because the old install is pinned to `0.2.0`; run `plugins install` explicitly. After `plugins unlink`, no `pulse` plugin is left, so install it again.
20
33
 
21
- ## Commands
34
+ A machine paired before the rename keeps its pairing, sign-in and state: the Keychain items, the `~/.config/agentpulse/hub` directory, the service label and the wire identifiers did not change. The installed launchd or systemd service still holds the absolute paths of the old install, though. If the daemon path changed, or the node binary it names was removed (for example by a node upgrade), `agentpulse pulse status`, `agentpulse pulse doctor` and `agentpulse pulse start` report `HUB_SERVICE_PATH_STALE`. Run `agentpulse pulse install` once, then `agentpulse pulse start`.
22
35
 
23
- | Command | What it does |
24
- |---|---|
25
- | `agentpulse pulse start --requirements "…" --name <name>` | Begin an engagement in the current repo (`.pulse/`). |
26
- | `agentpulse pulse next` | Emit the current phase work-list (prompts + JSON schema; contract injected). |
27
- | `agentpulse pulse submit --results-file r.json` | Validate your model's structured results vs schema + contract. |
28
- | `agentpulse pulse gate --approved [--decisions-file d.json]` | The human gate — advance the phase; lock product decisions. |
29
- | `agentpulse pulse verify` | Run the contract's gate command (`npm run check`); exits non-zero if red. |
30
- | `agentpulse pulse status` | Phase, contract, board progress. |
36
+ ## How it works
31
37
 
32
- The MCP server (`pulse-mcp`, shipped by `@agentpulselabs/pulse`) is the primary seam for interactive agents
33
- (Cursor/Codex/Claude). This CLI plugin is the universal shell fallback: same core, same `.pulse/` state.
38
+ The plugin manages a separate launchd (macOS) or systemd-user (Linux) companion. The oclif command process is never the daemon. Local configuration and state are owner-only. The OAuth tokens and the local Ed25519 key live only in Keychain or libsecret; if neither is available the connector fails closed.
34
39
 
35
- ## Architecture
40
+ Authentication is the platform's OAuth (Phase 1). `agentpulse pulse pair` and `agentpulse pulse login` run the same browser PKCE flow as `agentpulse login` (public client `agentpulse-cli`, loopback `127.0.0.1`), but keep their own token store and never write the CLI's `config.json`. The CLI config is only read for the default platform URL and dev-org slug.
36
41
 
37
- ```
38
- argv → src/commands/pulse/<verb>.js (thin oclif Command, parses flags)
39
- → src/runPulse.js (pure: verb → engine method; no oclif, testable)
40
- → @agentpulselabs/pulse (the brain: state machine + contract + validator + gates)
41
- → .pulse/ in the repo (durable, git-tracked truth)
42
- ```
42
+ Pairing resolves the dev-org through `/api/team/dev-orgs`, mints a dev-org token through `/api/team/dev-orgs/:id/cli-token`, and registers the machine with `POST /api/agent-hub/user/machines` and a frozen least-authority grant. Every connector request carries `Authorization: Bearer`, `X-Serverless-Authorization` on Cloud Run dev-orgs, and the machine, session and policy-version headers.
43
+
44
+ If the platform can't be reached to refresh the sign-in, the daemon reports `reconnecting`, claims nothing and retries with backoff. When a refresh is refused (`invalid_grant`), it stops claiming work and `agentpulse pulse status` prints the commands to run. Today that refusal is expected about every 12 hours, until the platform fixes refresh for the `agentpulse-cli` client. A revoked machine stops for good; signing in again never revives it. The Ed25519 identity is kept locally for Phase 2 (DPoP) and is not used for authentication.
45
+
46
+ The daemon restarts itself by exiting with code 75; launchd (`KeepAlive.SuccessfulExit=false`) and systemd (`Restart=on-failure`) start it again. It does so when any file under its own `src/` (data files such as `mirror-contract.json` included, dotfiles not) or its `package.json` changes (2 s after the last write, and only if the contents differ), when `restart.request` appears in its config directory, or when the cycle or a mirror pass makes no progress for 3 minutes (`cycle_stalled` in `daemon-error.log`). A code or request restart drains first: it claims no new job, lets running jobs finish and settle (health shows `restarting` with the job ids), and only aborts them, as `HUB_DAEMON_RESTARTING`, after 15 minutes. A stall aborts at once. A request older than the running process is dropped, since the restart it asked for already happened. `agentpulse pulse restart` writes that file and waits up to 60 s for a new process, so it needs no `launchctl` or `systemctl`; it refuses when the daemon has no fresh heartbeat. After more than 5 restarts in 5 minutes the daemon stays up instead, `agentpulse pulse status` reports `restart_loop`, and a pending restart is deferred until the window clears. `pulse stop` and SIGTERM always win and exit 0.
47
+
48
+ The connector accepts only registered intents:
49
+
50
+ - `workspace.read` and `workspace.write`
51
+ - `git.read` and `git.write`
52
+ - `worktree.create`
53
+ - `agent.cursor.invoke` and `agent.claude.invoke`
54
+ - `hub.write`, only after `agentpulse pulse hub-write on`
55
+
56
+ It does not accept shell commands, absolute cloud paths, package installation, secret reads, permission changes, push, release, production actions, or remote grant expansion.
57
+
58
+ ## Acting on the local Agent Hub from Pulse
59
+
60
+ `agentpulse pulse hub-write on` lets Pulse post to the local Agent Hub as you: a chat message, a NUDGE or STATUS? to agents, a decision or change request on a stream, accepting a delivery, or a reply to a bus ask. It is off on a new pairing. `on` and `off` replace this machine's cloud grant with `hub.write` added or removed (nothing else changes) and store the new policy version in the daemon config; the daemon adopts it on its next heartbeat. `status` reads the grant from Pulse.
61
+
62
+ Each action arrives as a typed `hub.*` job and becomes exactly one POST to the hub on `127.0.0.1` with the hub token, as the principal `user`, to `/api/chat/post`, `/api/message`, `/api/decision`, `/api/accept` or `/bus/ack`, and nothing else. If the hub may have taken a write whose result was never recorded (the daemon stopped mid-request, or the hub did not answer), the action ends as `hub_write_unconfirmed` instead of being sent twice; check the hub before sending it again. A hub that is not running gives `hub_unreachable`; a write the hub rejects gives `hub_refused` with its reason.
63
+
64
+ ## Mirroring the local Agent Hub
65
+
66
+ `agentpulse pulse mirror on` lets the paired daemon mirror your local Agent Hub into Pulse, so its deliveries, streams, agents, graph, chat (task, project, one-to-one and bus threads), inbox and Cursor rule packs show there read-only. It is off on a new install. `on` and `off` change only `mirror.localHub` in the daemon config, and the daemon picks the change up within a heartbeat. `off` stops pushing; items already mirrored stay in Pulse. `status` shows the last sync, how many items are mirrored, and how many were not mirrored, by kind and reason.
67
+
68
+ The mirror needs the `agent-hub:connector:mirror` scope. A sign-in from before this version lacks it; run `agentpulse pulse login` once.
69
+
70
+ How it reads and what it sends:
71
+
72
+ - It finds the hub from the `com.agentpulse.agents-hub` launch agent (or `mirror.hub.dir` and `mirror.hub.port` in the daemon config) and only sends GETs to an allow-list of endpoints on `127.0.0.1`, with the hub's `state/hub.token`. It never writes to the hub, never runs `hub.mjs`, and never opens `state/` files other than the token.
73
+ - It follows the hub's `/api/stream` feed, re-reads only the affected endpoints about 300 ms after a change, runs a full reconcile 60 seconds after the last one ended, and reconnects with backoff.
74
+ - The four graph windows (which also carry edges and bus messages) are the hub's slowest read. After a change they are re-read in a pass of their own, at most once every 3 seconds counted from the end of the last one. The hub announces each of the connector's own graph reads as a change, so a hub change that arrives during one of those reads or within 1.5 seconds after it is most likely an echo, so it re-reads the windows only after 10 seconds; any other change brings the 3 second gap back. Task briefs follow in the next pass, four at a time and for at most 3 seconds. `agentpulse pulse mirror status` shows how long the last pass took.
75
+ - Only new or changed items are sent, in batches of at most 500 items and 2 MiB. Items that disappear are deleted in Pulse on the full reconcile, and only for sources that were read completely.
76
+ - Absolute paths are rewritten before upload: paths in a known repo become repo-relative, workspace and home paths lose their prefix, and any other path keeps only its last segment. A home folder never shows its user name. An item that still carries a path, a token-looking string or the hub token is withheld and counted, never sent.
77
+ - Rule packs include only `*.mdc` under `.cursor/rules/`, plus `AGENTS.md` and `CLAUDE.md`, from the workspace, each repo and your user folder. Files named like `.env*`, `*secret*` or `*token*`, symlinks that leave the pack, and oversized files are skipped.
78
+ - Transcripts, diffs, raw provider output, the hub token, Keychain and OAuth material are never read for the mirror or sent.
43
79
 
44
- ## Test
80
+ ## Local checks
45
81
 
46
82
  ```bash
47
- node --test "test/**/*.test.js" # exercises runPulse over a real .pulse/, no @oclif/core needed
83
+ npm test
84
+ npm run check
48
85
  ```
package/package.json CHANGED
@@ -1,20 +1,10 @@
1
1
  {
2
2
  "name": "@agentpulselabs/cli-plugin-pulse",
3
- "version": "0.2.0",
4
- "description": "oclif plugin that adds the `pulse` topic (portable, contract-enforced build discipline) to the agentpulse CLI. User-installed: `agentpulse plugins install @agentpulselabs/cli-plugin-pulse`. A thin argv→core seam over @agentpulselabs/pulse.",
3
+ "version": "1.0.0",
4
+ "description": "AgentPulse CLI plugin for installing and operating the Pulse local connector (replaces the deprecated build-discipline Pulse plugin).",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "publishConfig": {
8
- "access": "public"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/ashishcloud/agentpulse-cli-plugin-pulse.git"
13
- },
14
- "homepage": "https://github.com/ashishcloud/agentpulse-cli-plugin-pulse#readme",
15
- "bugs": {
16
- "url": "https://github.com/ashishcloud/agentpulse-cli-plugin-pulse/issues"
17
- },
7
+ "private": false,
18
8
  "files": [
19
9
  "src",
20
10
  "README.md"
@@ -24,12 +14,13 @@
24
14
  "topicSeparator": " ",
25
15
  "topics": {
26
16
  "pulse": {
27
- "description": "Run a contract-enforced build discipline engagement (Discovery → Stories → Architecture → Build → Deliver)."
17
+ "description": "Install, pair, and operate the Pulse local connector."
28
18
  }
29
19
  }
30
20
  },
31
21
  "scripts": {
32
- "test": "node --test \"test/**/*.test.js\""
22
+ "test": "node --import ./test/support/register-loader.mjs --test \"test/**/*.test.js\"",
23
+ "check": "node --check src/commands/pulse/install.js && node --check src/commands/pulse/pair.js && node --check src/commands/pulse/login.js && node --check src/commands/pulse/start.js && node --check src/commands/pulse/status.js && node --check src/commands/pulse/doctor.js && node --check src/commands/pulse/stop.js && node --check src/commands/pulse/restart.js && node --check src/lib/selfRestart.js && node --check src/lib/subprocess.js && node --check src/lib/daemonProcess.js && node --check src/commands/pulse/mirror.js && node --check src/commands/pulse/hub-write.js && node --check src/lib/localHubWriter.js && node --check src/lib/hubActions.js && node --check src/lib/localHub.js && node --check src/lib/pathRedaction.js && node --check src/lib/rulePacks.js && node --check src/lib/mirrorContract.js && node --check src/lib/graphWindows.js && node --check src/lib/mirrorSource.js && node --check src/lib/mirrorSync.js && node --check src/lib/mirrorService.js && node --check src/lib/mirrorDaemon.js && node --check src/lib/daemonComposition.js && node --check src/lib/daemonRuntime.js && node --check src/lib/daemonLoop.js && node --check src/daemon/main.js"
33
24
  },
34
25
  "engines": {
35
26
  "node": ">=20"
@@ -38,12 +29,13 @@
38
29
  "oclif-plugin",
39
30
  "agentpulse",
40
31
  "pulse",
32
+ "agent-hub",
41
33
  "cli"
42
34
  ],
43
- "dependencies": {
44
- "@agentpulselabs/pulse": "^0.1.0"
45
- },
46
35
  "peerDependencies": {
47
36
  "@oclif/core": ">=3"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
48
40
  }
49
41
  }
@@ -0,0 +1,14 @@
1
+ import BaseCommand from '../../lib/BaseCommand.js';
2
+ import { getDefaultApplication } from '../../lib/application.js';
3
+
4
+ export default class PulseDoctor extends BaseCommand {
5
+ static description = 'Diagnose the Pulse companion installation and local providers.';
6
+ static examples = ['<%= config.bin %> pulse doctor'];
7
+
8
+ async run() {
9
+ const { flags } = await this.parse(PulseDoctor);
10
+ const result = await (this.runtime || getDefaultApplication()).doctor(flags);
11
+ this.log(result.message);
12
+ return result;
13
+ }
14
+ }
@@ -0,0 +1,26 @@
1
+ import { Args } from '@oclif/core';
2
+ import BaseCommand from '../../lib/BaseCommand.js';
3
+ import { getDefaultApplication } from '../../lib/application.js';
4
+
5
+ export default class PulseHubWrite extends BaseCommand {
6
+ static description = 'Let Pulse post to the local Agent Hub as you (the hub.write grant), stop it, or show whether it can.';
7
+ static examples = [
8
+ '<%= config.bin %> pulse hub-write status',
9
+ '<%= config.bin %> pulse hub-write on',
10
+ '<%= config.bin %> pulse hub-write off',
11
+ ];
12
+ static args = {
13
+ action: Args.string({
14
+ description: 'on, off or status',
15
+ options: ['on', 'off', 'status'],
16
+ default: 'status',
17
+ }),
18
+ };
19
+
20
+ async run() {
21
+ const { args } = await this.parse(PulseHubWrite);
22
+ const result = await (this.runtime || getDefaultApplication()).hubWrite({ action: args.action });
23
+ this.log(result.message);
24
+ return result;
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ import BaseCommand from '../../lib/BaseCommand.js';
2
+ import { getDefaultApplication } from '../../lib/application.js';
3
+
4
+ export default class PulseInstall extends BaseCommand {
5
+ static description = 'Install or atomically update the Pulse companion daemon service.';
6
+ static examples = ['<%= config.bin %> pulse install'];
7
+
8
+ async run() {
9
+ const { flags } = await this.parse(PulseInstall);
10
+ const result = await (this.runtime || getDefaultApplication()).install(flags);
11
+ this.log(result.message);
12
+ return result;
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../lib/BaseCommand.js';
3
+ import { getDefaultApplication } from '../../lib/application.js';
4
+
5
+ export default class PulseLogin extends BaseCommand {
6
+ static description = 'Sign the Pulse connector in again with your AgentPulse account (browser OAuth with PKCE).';
7
+ static examples = ['<%= config.bin %> pulse login', '<%= config.bin %> pulse login --platform https://app.agentpulse.example.com'];
8
+ static flags = {
9
+ platform: Flags.string({
10
+ description: 'Platform URL to sign in to (defaults to the paired platform).',
11
+ }),
12
+ };
13
+
14
+ async run() {
15
+ const { flags } = await this.parse(PulseLogin);
16
+ const result = await (this.runtime || getDefaultApplication()).login({ platformUrl: flags.platform });
17
+ this.log(result.message);
18
+ return result;
19
+ }
20
+ }
@@ -0,0 +1,27 @@
1
+ import { Args } from '@oclif/core';
2
+ import BaseCommand from '../../lib/BaseCommand.js';
3
+ import { getDefaultApplication } from '../../lib/application.js';
4
+
5
+ export default class PulseMirror extends BaseCommand {
6
+ static description = 'Turn mirroring of the local Agent Hub into Pulse on or off, show its status, or send the items Pulse refused again.';
7
+ static examples = [
8
+ '<%= config.bin %> pulse mirror status',
9
+ '<%= config.bin %> pulse mirror on',
10
+ '<%= config.bin %> pulse mirror off',
11
+ '<%= config.bin %> pulse mirror resend-rejected',
12
+ ];
13
+ static args = {
14
+ action: Args.string({
15
+ description: 'on, off, status or resend-rejected',
16
+ options: ['on', 'off', 'status', 'resend-rejected'],
17
+ default: 'status',
18
+ }),
19
+ };
20
+
21
+ async run() {
22
+ const { args } = await this.parse(PulseMirror);
23
+ const result = await (this.runtime || getDefaultApplication()).mirror({ action: args.action });
24
+ this.log(result.message);
25
+ return result;
26
+ }
27
+ }
@@ -0,0 +1,38 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../lib/BaseCommand.js';
3
+ import { getDefaultApplication } from '../../lib/application.js';
4
+
5
+ export default class PulsePair extends BaseCommand {
6
+ static description = 'Pair this machine with Pulse using your AgentPulse sign-in.';
7
+ static examples = [
8
+ '<%= config.bin %> pulse pair --org builddevorg8 --root agent-hub=~/src/agent-hub',
9
+ '<%= config.bin %> pulse pair --platform https://app.agentpulse.example.com --org my-dev-org --root app=.',
10
+ ];
11
+ static flags = {
12
+ org: Flags.string({
13
+ description: 'Dev-org slug that hosts Pulse (defaults to the agentpulse CLI active org).',
14
+ }),
15
+ platform: Flags.string({
16
+ description: 'Platform URL to sign in to (defaults to the agentpulse CLI active profile).',
17
+ }),
18
+ name: Flags.string({
19
+ description: 'Display name for this machine.',
20
+ }),
21
+ root: Flags.string({
22
+ description: 'Approve a local root as <rootId>=<path> (repeatable).',
23
+ multiple: true,
24
+ }),
25
+ };
26
+
27
+ async run() {
28
+ const { flags } = await this.parse(PulsePair);
29
+ const result = await (this.runtime || getDefaultApplication()).pair({
30
+ org: flags.org,
31
+ platformUrl: flags.platform,
32
+ displayName: flags.name,
33
+ roots: flags.root || [],
34
+ });
35
+ this.log(result.message);
36
+ return result;
37
+ }
38
+ }
@@ -0,0 +1,14 @@
1
+ import BaseCommand from '../../lib/BaseCommand.js';
2
+ import { getDefaultApplication } from '../../lib/application.js';
3
+
4
+ export default class PulseRestart extends BaseCommand {
5
+ static description = 'Ask the running Pulse daemon to restart itself and wait for the new process (no launchctl or systemctl needed).';
6
+ static examples = ['<%= config.bin %> pulse restart'];
7
+
8
+ async run() {
9
+ await this.parse(PulseRestart);
10
+ const result = await (this.runtime || getDefaultApplication()).restart();
11
+ this.log(result.message);
12
+ return result;
13
+ }
14
+ }
@@ -1,26 +1,14 @@
1
- import { Flags } from '@oclif/core';
2
1
  import BaseCommand from '../../lib/BaseCommand.js';
3
- import { runPulse } from '../../runPulse.js';
2
+ import { getDefaultApplication } from '../../lib/application.js';
4
3
 
5
- /** `agentpulse pulse start` — begin an engagement in the current repo (.pulse/). Thin argv→core shim. */
6
4
  export default class PulseStart extends BaseCommand {
7
- static description = 'Begin a Pulse engagement in the current repo (.pulse/).';
8
- static examples = ['<%= config.bin %> pulse start --requirements "Build a widget store" --name widgets'];
9
- static flags = {
10
- requirements: Flags.string({ description: 'Raw requirements text (or use --requirements-file).', required: false }),
11
- 'requirements-file': Flags.string({ description: 'Path to a file containing the requirements.' }),
12
- grounding: Flags.string({ description: 'Optional domain grounding notes.' }),
13
- name: Flags.string({ description: 'Short engagement name (used for filenames).', default: 'engagement' }),
14
- };
5
+ static description = 'Start the installed Pulse companion daemon.';
6
+ static examples = ['<%= config.bin %> pulse start'];
15
7
 
16
8
  async run() {
17
9
  const { flags } = await this.parse(PulseStart);
18
- const fs = await import('node:fs');
19
- const requirements = flags['requirements-file']
20
- ? fs.readFileSync(flags['requirements-file'], 'utf8')
21
- : (flags.requirements || '');
22
- const result = await runPulse('start', { requirements, grounding: flags.grounding, name: flags.name });
23
- this.log(JSON.stringify(result, null, 2)); // default: raw JSON (auto-silenced under --json)
24
- return result; // --json: standard envelope
10
+ const result = await (this.runtime || getDefaultApplication()).start(flags);
11
+ this.log(result.message);
12
+ return result;
25
13
  }
26
14
  }
@@ -1,14 +1,14 @@
1
1
  import BaseCommand from '../../lib/BaseCommand.js';
2
- import { runPulse } from '../../runPulse.js';
2
+ import { getDefaultApplication } from '../../lib/application.js';
3
3
 
4
- /** `agentpulse pulse status` — where does the engagement stand? Phase, contract, board progress. */
5
4
  export default class PulseStatus extends BaseCommand {
6
- static description = 'Show engagement status: phase, contract, board progress.';
5
+ static description = 'Show Pulse companion service, pairing, and health status.';
7
6
  static examples = ['<%= config.bin %> pulse status'];
8
7
 
9
8
  async run() {
10
- const result = await runPulse('status', {});
11
- this.log(JSON.stringify(result, null, 2)); // default: raw JSON (auto-silenced under --json)
12
- return result; // --json: standard envelope
9
+ const { flags } = await this.parse(PulseStatus);
10
+ const result = await (this.runtime || getDefaultApplication()).status(flags);
11
+ this.log(result.message);
12
+ return result;
13
13
  }
14
14
  }
@@ -0,0 +1,24 @@
1
+ import { Flags } from '@oclif/core';
2
+ import BaseCommand from '../../lib/BaseCommand.js';
3
+ import { getDefaultApplication } from '../../lib/application.js';
4
+
5
+ export default class PulseStop extends BaseCommand {
6
+ static description = 'Stop the Pulse companion and optionally revoke this machine.';
7
+ static examples = [
8
+ '<%= config.bin %> pulse stop',
9
+ '<%= config.bin %> pulse stop --revoke',
10
+ ];
11
+ static flags = {
12
+ revoke: Flags.boolean({
13
+ description: 'Revoke this machine after stopping the companion.',
14
+ default: false,
15
+ }),
16
+ };
17
+
18
+ async run() {
19
+ const { flags } = await this.parse(PulseStop);
20
+ const result = await (this.runtime || getDefaultApplication()).stop(flags);
21
+ this.log(result.message);
22
+ return result;
23
+ }
24
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { defaultConfigDirectory } from '../lib/application.js';
5
+ import { runDaemon } from '../lib/daemonProcess.js';
6
+ import { ServiceManager } from '../lib/serviceManager.js';
7
+
8
+ const MIRROR_FAILURES = new Set(['mirror_failed', 'mirror_blocked', 'mirror_stream_failed']);
9
+ const ERROR_EVENTS = new Set([
10
+ 'job_failed', 'job_refused', 'job_settle_failed', 'cycle_stalled', 'drain_timed_out', 'daemon_start_failed',
11
+ 'daemon_shutdown_failed', 'source_watch_refused', 'source_watch_failed',
12
+ ]);
13
+
14
+ /** One redacted line per job, local hub or restart event; failures go to daemon-error.log, the rest to daemon.log. */
15
+ function logEvent(event, details) {
16
+ const line = `Pulse ${event} ${JSON.stringify({ at: new Date().toISOString(), ...details })}\n`;
17
+ (ERROR_EVENTS.has(event) ? process.stderr : process.stdout).write(line);
18
+ }
19
+
20
+ function configDirectory(argv) {
21
+ const index = argv.indexOf('--config-dir');
22
+ return index === -1 ? defaultConfigDirectory() : path.resolve(argv[index + 1]);
23
+ }
24
+
25
+ runDaemon({
26
+ directory: configDirectory(process.argv.slice(2)),
27
+ pluginRoot: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
28
+ logDir: new ServiceManager({}).logDir,
29
+ log: logEvent,
30
+ mirrorLog: (event, details) => {
31
+ if (MIRROR_FAILURES.has(event)) process.stderr.write(`Pulse ${event}: ${details.code}\n`);
32
+ },
33
+ }).catch((error) => {
34
+ process.stderr.write(`Pulse daemon failed: ${error.code || 'HUB_DAEMON_FAILED'}: ${error.message}\n`);
35
+ process.exitCode = 1;
36
+ });
@@ -1,37 +1,37 @@
1
1
  import { Command } from '@oclif/core';
2
2
 
3
- /**
4
- * BaseCommand — the pulse plugin's half of the machine-readable CLI contract (docs/PLAN.md M0), matching the host
5
- * CLI's BaseCommand so `agentpulse pulse <verb> --json` yields the SAME standard envelope as first-party commands:
6
- *
7
- * success: { tool:'agentpulse', command:'pulse status', version, ok:true, result }
8
- * error: { tool:'agentpulse', command:'pulse status', version, ok:false, error:{ code, message } }
9
- *
10
- * Behavior note (deliberate, back-compat): pulse output is machine-oriented (work-lists with prompts + JSON
11
- * schemas an agent consumes), so the DEFAULT (no --json) still prints the raw engine result as pretty JSON —
12
- * exactly as before. `--json` layers the standard envelope on top. Both are JSON; --json is the stable contract.
13
- */
14
3
  export default class BaseCommand extends Command {
15
4
  static enableJsonFlag = true;
16
5
 
17
6
  toSuccessJson(result) {
18
- const marked = result && typeof result === 'object' && result.__ap === true;
19
- const ok = marked ? result.ok : true;
20
- const payload = marked ? result.result : result;
21
- return { tool: 'agentpulse', command: this.commandName(), version: this.config?.version, ok, result: payload };
7
+ return {
8
+ tool: 'agentpulse',
9
+ command: this.commandName(),
10
+ version: this.config?.version,
11
+ ok: true,
12
+ result,
13
+ };
22
14
  }
23
15
 
24
- toErrorJson(err) {
16
+ toErrorJson(error) {
25
17
  return {
26
- tool: 'agentpulse', command: this.commandName(), version: this.config?.version, ok: false,
27
- error: { code: (err && err.code) || 'E_ERROR', message: (err && err.message) || String(err) },
18
+ tool: 'agentpulse',
19
+ command: this.commandName(),
20
+ version: this.config?.version,
21
+ ok: false,
22
+ error: {
23
+ code: error?.code || 'HUB_ERROR',
24
+ message: error?.message || String(error),
25
+ },
28
26
  };
29
27
  }
30
28
 
31
- commandName() { return String(this.id || this.ctor?.id || '').replace(/:/g, ' '); }
32
-
33
- fail(code, message, exit = 1) { process.exitCode = exit; this.error(message, { code, exit }); }
29
+ commandName() {
30
+ return String(this.id || this.ctor?.id || '').replace(/:/g, ' ');
31
+ }
34
32
 
35
- /** A successful run that reports ok:false and exits non-zero, carrying its full payload (e.g. `verify` red). */
36
- problem(payload, exit = 1) { process.exitCode = exit; return { __ap: true, ok: false, result: payload }; }
33
+ fail(code, message, exit = 1) {
34
+ process.exitCode = exit;
35
+ this.error(message, { code, exit });
36
+ }
37
37
  }