@agentpulselabs/cli-plugin-pulse 0.1.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.
- package/README.md +48 -0
- package/package.json +41 -0
- package/src/commands/pulse/gate.js +23 -0
- package/src/commands/pulse/next.js +13 -0
- package/src/commands/pulse/start.js +24 -0
- package/src/commands/pulse/status.js +13 -0
- package/src/commands/pulse/submit.js +19 -0
- package/src/commands/pulse/verify.js +18 -0
- package/src/runPulse.js +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @agentpulselabs/cli-plugin-pulse
|
|
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`](../pulse); all discipline lives in the core.
|
|
6
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```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
|
|
16
|
+
```
|
|
17
|
+
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
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. |
|
|
31
|
+
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
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
|
+
```
|
|
43
|
+
|
|
44
|
+
## Test
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node --test "test/**/*.test.js" # exercises runPulse over a real .pulse/, no @oclif/core needed
|
|
48
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentpulselabs/cli-plugin-pulse",
|
|
3
|
+
"version": "0.1.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.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"oclif": {
|
|
15
|
+
"commands": "./src/commands",
|
|
16
|
+
"topicSeparator": " ",
|
|
17
|
+
"topics": {
|
|
18
|
+
"pulse": {
|
|
19
|
+
"description": "Run a contract-enforced build discipline engagement (Discovery → Stories → Architecture → Build → Deliver)."
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node --test \"test/**/*.test.js\""
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"oclif-plugin",
|
|
31
|
+
"agentpulse",
|
|
32
|
+
"pulse",
|
|
33
|
+
"cli"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@agentpulselabs/pulse": "^0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@oclif/core": ">=3"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { runPulse } from '../../runPulse.js';
|
|
3
|
+
|
|
4
|
+
/** `agentpulse pulse gate` — the human-in-the-loop barrier. Advance only with --approved. */
|
|
5
|
+
export default class PulseGate extends Command {
|
|
6
|
+
static description = 'The human gate. Advance the phase ONLY with --approved. Lock product decisions here.';
|
|
7
|
+
static examples = ['<%= config.bin %> pulse gate --approved --decisions-file decisions.json'];
|
|
8
|
+
static flags = {
|
|
9
|
+
approved: Flags.boolean({ description: 'Approve this phase (silence/omission = not approved).', default: false }),
|
|
10
|
+
'decisions-file': Flags.string({ description: 'Path to a JSON file of human product decisions to lock: [{ title, decision, rationale }].' }),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async run() {
|
|
14
|
+
const { flags } = await this.parse(PulseGate);
|
|
15
|
+
let decisions = [];
|
|
16
|
+
if (flags['decisions-file']) {
|
|
17
|
+
const fs = await import('node:fs');
|
|
18
|
+
decisions = JSON.parse(fs.readFileSync(flags['decisions-file'], 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
const result = await runPulse('gate', { approved: flags.approved, decisions });
|
|
21
|
+
this.log(JSON.stringify(result, null, 2));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { runPulse } from '../../runPulse.js';
|
|
3
|
+
|
|
4
|
+
/** `agentpulse pulse next` — emit the current phase work-list (prompts + schema, contract injected). */
|
|
5
|
+
export default class PulseNext extends Command {
|
|
6
|
+
static description = 'Emit the current phase work-list: tasks (prompt + JSON schema) your model must run.';
|
|
7
|
+
static examples = ['<%= config.bin %> pulse next'];
|
|
8
|
+
|
|
9
|
+
async run() {
|
|
10
|
+
const result = await runPulse('next', {});
|
|
11
|
+
this.log(JSON.stringify(result, null, 2));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { runPulse } from '../../runPulse.js';
|
|
3
|
+
|
|
4
|
+
/** `agentpulse pulse start` — begin an engagement in the current repo (.pulse/). Thin argv→core shim. */
|
|
5
|
+
export default class PulseStart extends Command {
|
|
6
|
+
static description = 'Begin a Pulse engagement in the current repo (.pulse/).';
|
|
7
|
+
static examples = ['<%= config.bin %> pulse start --requirements "Build a widget store" --name widgets'];
|
|
8
|
+
static flags = {
|
|
9
|
+
requirements: Flags.string({ description: 'Raw requirements text (or use --requirements-file).', required: false }),
|
|
10
|
+
'requirements-file': Flags.string({ description: 'Path to a file containing the requirements.' }),
|
|
11
|
+
grounding: Flags.string({ description: 'Optional domain grounding notes.' }),
|
|
12
|
+
name: Flags.string({ description: 'Short engagement name (used for filenames).', default: 'engagement' }),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
async run() {
|
|
16
|
+
const { flags } = await this.parse(PulseStart);
|
|
17
|
+
const fs = await import('node:fs');
|
|
18
|
+
const requirements = flags['requirements-file']
|
|
19
|
+
? fs.readFileSync(flags['requirements-file'], 'utf8')
|
|
20
|
+
: (flags.requirements || '');
|
|
21
|
+
const result = await runPulse('start', { requirements, grounding: flags.grounding, name: flags.name });
|
|
22
|
+
this.log(JSON.stringify(result, null, 2));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import { runPulse } from '../../runPulse.js';
|
|
3
|
+
|
|
4
|
+
/** `agentpulse pulse status` — where does the engagement stand? Phase, contract, board progress. */
|
|
5
|
+
export default class PulseStatus extends Command {
|
|
6
|
+
static description = 'Show engagement status: phase, contract, board progress.';
|
|
7
|
+
static examples = ['<%= config.bin %> pulse status'];
|
|
8
|
+
|
|
9
|
+
async run() {
|
|
10
|
+
const result = await runPulse('status', {});
|
|
11
|
+
this.log(JSON.stringify(result, null, 2));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { runPulse } from '../../runPulse.js';
|
|
3
|
+
|
|
4
|
+
/** `agentpulse pulse submit` — validate the model's structured results against schema + contract. */
|
|
5
|
+
export default class PulseSubmit extends Command {
|
|
6
|
+
static description = "Submit your model's structured results (JSON). Pulse validates them vs schema + contract.";
|
|
7
|
+
static examples = ['<%= config.bin %> pulse submit --results-file discovery-results.json'];
|
|
8
|
+
static flags = {
|
|
9
|
+
'results-file': Flags.string({ description: 'Path to a JSON file: [{ id, data }, …] (one per work-list task).', required: true }),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(PulseSubmit);
|
|
14
|
+
const fs = await import('node:fs');
|
|
15
|
+
const results = JSON.parse(fs.readFileSync(flags['results-file'], 'utf8'));
|
|
16
|
+
const result = await runPulse('submit', { results });
|
|
17
|
+
this.log(JSON.stringify(result, null, 2));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import { runPulse } from '../../runPulse.js';
|
|
3
|
+
|
|
4
|
+
/** `agentpulse pulse verify` — run the contract's deterministic gate command. "Done" is this exit code. */
|
|
5
|
+
export default class PulseVerify extends Command {
|
|
6
|
+
static description = "Run the contract's gate command (e.g. `npm run check`) and report pass/fail. Never an LLM opinion.";
|
|
7
|
+
static examples = ['<%= config.bin %> pulse verify'];
|
|
8
|
+
static flags = {
|
|
9
|
+
cwd: Flags.string({ description: 'Directory to run the gate in (default: current repo).' }),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
const { flags } = await this.parse(PulseVerify);
|
|
14
|
+
const result = await runPulse('verify', { cwd: flags.cwd });
|
|
15
|
+
this.log(JSON.stringify(result, null, 2));
|
|
16
|
+
if (!result.green) this.exit(1); // non-zero so CI / scripts can gate on it
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/runPulse.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runPulse.js — the PURE command core for the pulse CLI topic. Maps a verb + args → an engine call → a
|
|
3
|
+
* JSON-serializable result. No oclif, no I/O of its own (the engine's state.js owns fs). This is what makes
|
|
4
|
+
* the plugin testable without installing @oclif/core, and keeps the seam thin: the oclif command classes
|
|
5
|
+
* (src/commands/pulse/*.js) are ~5-line wrappers that parse flags and call this.
|
|
6
|
+
*
|
|
7
|
+
* Mirror of the MCP seam's tools.js: one verb == one engine method. All discipline lives in @agentpulselabs/pulse.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createEngine } from '@agentpulselabs/pulse';
|
|
11
|
+
|
|
12
|
+
/** The verbs this topic exposes, each → an engine method. Kept in sync with the MCP tool surface. */
|
|
13
|
+
export const ACTIONS = ['start', 'next', 'submit', 'gate', 'verify', 'status', 'board', 'claim', 'sync'];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} action - one of ACTIONS
|
|
17
|
+
* @param {Object} args - parsed flags/inputs for the action
|
|
18
|
+
* @param {Object} [opts] - { cwd, createEngine } (createEngine injectable for tests)
|
|
19
|
+
* @returns {Promise<Object>} JSON-serializable result
|
|
20
|
+
*/
|
|
21
|
+
export async function runPulse(action, args = {}, opts = {}) {
|
|
22
|
+
const make = opts.createEngine || createEngine;
|
|
23
|
+
const engine = make({ cwd: opts.cwd || process.cwd() });
|
|
24
|
+
|
|
25
|
+
switch (action) {
|
|
26
|
+
case 'start':
|
|
27
|
+
return engine.start({ requirements: args.requirements, grounding: args.grounding, name: args.name });
|
|
28
|
+
case 'next':
|
|
29
|
+
return engine.next();
|
|
30
|
+
case 'submit':
|
|
31
|
+
return engine.submit({ results: parseResults(args.results) });
|
|
32
|
+
case 'gate':
|
|
33
|
+
return engine.gate({ approved: !!args.approved, decisions: parseJson(args.decisions, []) });
|
|
34
|
+
case 'verify':
|
|
35
|
+
return await engine.verify({ cwd: args.cwd });
|
|
36
|
+
case 'status':
|
|
37
|
+
return engine.status();
|
|
38
|
+
case 'board':
|
|
39
|
+
return engine.boardUpdate({ id: args.id, status: args.status });
|
|
40
|
+
case 'claim':
|
|
41
|
+
return engine.claim({ id: args.id, agentId: args.agentId });
|
|
42
|
+
case 'sync':
|
|
43
|
+
return engine.sync({ ids: parseJson(args.ids, undefined) });
|
|
44
|
+
default:
|
|
45
|
+
throw new Error(`unknown pulse action "${action}" — expected one of ${ACTIONS.join(', ')}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// results may arrive as a JSON string (from --results-file content) or already-parsed array.
|
|
50
|
+
function parseResults(r) {
|
|
51
|
+
if (Array.isArray(r)) return r;
|
|
52
|
+
return parseJson(r, []);
|
|
53
|
+
}
|
|
54
|
+
function parseJson(v, fallback) {
|
|
55
|
+
if (v == null) return fallback;
|
|
56
|
+
if (typeof v !== 'string') return v;
|
|
57
|
+
try { return JSON.parse(v); } catch { return fallback; }
|
|
58
|
+
}
|