@rness/cli 0.18.0 → 0.19.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/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  workspaceDirs,
63
63
  writeFileAtomic,
64
64
  writeManifest
65
- } from "./chunk-VGBS7HDY.js";
65
+ } from "./chunk-U5SBNWVD.js";
66
66
 
67
67
  // src/index.ts
68
68
  async function run2(argv) {
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "rness",
4
+ "description": "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan.",
5
+ "author": {
6
+ "name": "rness-dev",
7
+ "url": "https://rness.dev"
8
+ },
9
+ "homepage": "https://rness.dev",
10
+ "repository": "https://github.com/rness-dev/rness",
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "rness",
14
+ "agents",
15
+ "claude-code",
16
+ "adr",
17
+ "specification",
18
+ "plan"
19
+ ],
20
+ "types": "./types/index.d.ts"
21
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }
@@ -0,0 +1,188 @@
1
+ import { atom, read, update } from 'claude-code'
2
+ import type { EngineInterface, Register } from 'claude-code'
3
+
4
+ import type { RnessSnapshot } from '../types'
5
+
6
+ // The rness plugin's mod (spec 0029 §3): the workspace on screen, drawn from
7
+ // what the pinned CLI prints (`rness status --json`). Nothing is computed
8
+ // here: the CLI words every line, the module only draws and refreshes.
9
+ //
10
+ // `sync` writes this file whole into `.claude/skills/rness/hooks/`, at the
11
+ // workspace root and at the root of each clone, with `RNESS` set to the path
12
+ // of `.rness` from that place, as the settings hooks have it. One path, never
13
+ // a guess: a clone runs the workspace's pinned copy, never one its own tree
14
+ // could hold.
15
+
16
+ const snapshot = atom({ plugin: 'rness', key: 'snapshot' } as const, null)
17
+ const tab = atom({ plugin: 'rness', key: 'tab' } as const, 0)
18
+ const interactive = atom(
19
+ { plugin: 'rness', key: 'interactive' } as const,
20
+ false
21
+ )
22
+
23
+ const PANE = 'rness'
24
+ const BIN = 'node_modules/@rness/cli/dist/bin/rness.js'
25
+ /** `.rness` from the project directory; `sync` writes `../../.rness` in a clone. */
26
+ const RNESS = '.rness'
27
+ const STATE = /[\\/]\.rness[\\/]/
28
+
29
+ /** The project directory: the plugin lives in its `.claude/skills/rness`. */
30
+ const projectOf = (root: string): string =>
31
+ root.replace(/[\\/]\.claude[\\/]skills[\\/]rness[\\/]?$/, '')
32
+
33
+ /**
34
+ * Reads the snapshot from the pinned CLI and keeps it; the status line
35
+ * follows. No pinned copy, or a run that fails: no snapshot, nothing drawn.
36
+ */
37
+ async function refresh($: EngineInterface): Promise<RnessSnapshot | null> {
38
+ let next: RnessSnapshot | null = null
39
+ try {
40
+ const project = projectOf($.plugin.root)
41
+ const bin = `${project}/${RNESS}/${BIN}`
42
+ if (await $.fs.exists(bin)) {
43
+ const ran = await $.process.run(
44
+ ['node', bin, 'status', '--json', '--cwd', project],
45
+ { cwd: project, timeoutMs: 20_000 }
46
+ )
47
+ if (ran.exitCode === 0) next = JSON.parse(ran.stdout) as RnessSnapshot
48
+ }
49
+ } catch {
50
+ next = null
51
+ }
52
+ await update($, snapshot, () => next)
53
+ $.ui.status(next?.statusLine)
54
+ return next
55
+ }
56
+
57
+ /** A refresh off the dispatch that asked for it: a turn never waits for node. */
58
+ const later = ($: EngineInterface): void => {
59
+ $.clock.after(0, () => void refresh($))
60
+ }
61
+
62
+ export const register: Register = (on) => {
63
+ on('session.start', async ($, e, next) => {
64
+ await update($, interactive, () => e.isInteractive)
65
+ later($)
66
+ return next(e)
67
+ })
68
+
69
+ // An edit of `.rness/` changes what the band and the pane show.
70
+ on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
71
+ const ran = await next(e)
72
+ if (STATE.test(e.file_path)) later($)
73
+ return ran
74
+ })
75
+ on('tool.call', { tool: 'Write' }, async ($, e, next) => {
76
+ const ran = await next(e)
77
+ if (STATE.test(e.file_path)) later($)
78
+ return ran
79
+ })
80
+
81
+ // What no tool shows: a `git pull`, a file written through Bash.
82
+ on('turn.complete', async ($, e, next) => {
83
+ if (e.agentId === undefined) later($)
84
+ return next(e)
85
+ })
86
+
87
+ // The notes to act on, whenever there are some (plan 0042). The banner is
88
+ // the session-start line's, said once in the transcript: drawn here too,
89
+ // it said the same thing twice.
90
+ on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
91
+ const shown = await read($, snapshot)
92
+ if (e.props.hasSurvey || shown === null || shown.notes.length === 0)
93
+ return next(e)
94
+ const { Box, Text } = $.ui.resolve(e)
95
+ return (
96
+ <Box flexDirection="column">
97
+ {shown.notes.map((note, i) => (
98
+ <Box key={`note-${i}`}>
99
+ <Text color="yellow" wrap="truncate-end">
100
+ {note}
101
+ </Text>
102
+ </Box>
103
+ ))}
104
+ </Box>
105
+ )
106
+ })
107
+
108
+ // `/rness:status` is the skill's name: answered here with the pane, it
109
+ // runs no model turn; with no snapshot, or no one to see a pane, the
110
+ // skill's tables answer it.
111
+ on('command.run', { command: 'rness:status' }, async ($, e, next) => {
112
+ // Nobody to see a pane (`claude -p`): the tables are the answer.
113
+ if (!(await $.state.get({ plugin: 'rness', key: 'interactive' })).value)
114
+ return next(e)
115
+ const shown =
116
+ (await $.state.get({ plugin: 'rness', key: 'snapshot' })).value ??
117
+ (await refresh($))
118
+ if (shown === null || shown === undefined) return next(e)
119
+ const wanted = e.args.trim().toLowerCase()
120
+ const index = shown.tabs.findIndex(
121
+ (t) => t.name.toLowerCase() === wanted || t.label.toLowerCase() === wanted
122
+ )
123
+ await update($, tab, (i) => (index === -1 ? i : index))
124
+ // The pane takes the keys (plan 0042): a digit or Tab then Enter shows a
125
+ // tab, ↑/↓ scroll as in the full-screen `rness status`, Esc and `q`
126
+ // close it. A pane binds no ←/→ (Claude Code 2.1.289).
127
+ const opened = await $.ui.open({
128
+ id: PANE,
129
+ title: `${shown.workspace} · status`,
130
+ focus: true,
131
+ closeOnEscape: true,
132
+ })
133
+ // A surface that places no pane (an older desktop): the tables instead.
134
+ if (!opened.isPlaced) return next(e)
135
+ return {
136
+ text: 'The status of the workspace is open beside the conversation.',
137
+ }
138
+ })
139
+
140
+ on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
141
+ const { Box, Button, Text } = $.ui.resolve(e)
142
+ const shown = await read($, snapshot)
143
+ if (shown === null)
144
+ return <Text dimColor>rness: the workspace status cannot be read.</Text>
145
+ const at = Math.min(await read($, tab), Math.max(0, shown.tabs.length - 1))
146
+ const current = shown.tabs[at]
147
+ return (
148
+ <Box flexDirection="column">
149
+ <Box flexDirection="row" gap={1} flexWrap="wrap">
150
+ {shown.tabs.map((t, i) => (
151
+ <Button
152
+ key={`tab-${t.name}`}
153
+ // Where the body has nothing to scroll, ↑/↓ walk these buttons:
154
+ // only a press (Enter, a digit, a click) changes the tab.
155
+ label={`${i < 9 ? `${i + 1} ` : ''}${t.label} (${t.rows.length})`}
156
+ {...(i < 9 ? { hotkey: String(i + 1) } : {})}
157
+ variant={i === at ? 'primary' : 'secondary'}
158
+ {...(i === at ? { autoFocus: true as const } : {})}
159
+ onPress={() => update($, tab, () => i)}
160
+ />
161
+ ))}
162
+ <Button
163
+ key="close"
164
+ label="Close"
165
+ plain
166
+ hotkey="q"
167
+ role="dismiss"
168
+ onPress={() => $.ui.close({ id: PANE })}
169
+ />
170
+ </Box>
171
+ {current === undefined || current.rows.length === 0 ? (
172
+ <Box key="empty">
173
+ <Text dimColor>Nothing here yet.</Text>
174
+ </Box>
175
+ ) : (
176
+ // Every row: the engine scrolls the body (↑/↓, PgUp/PgDn, Home/End).
177
+ current.rows.map((row) => (
178
+ <Box key={`row-${row.id}`}>
179
+ <Text wrap="truncate-end">
180
+ {row.id} {row.title} <Text dimColor>{row.status ?? '?'}</Text>
181
+ </Text>
182
+ </Box>
183
+ ))
184
+ )}
185
+ </Box>
186
+ )
187
+ })
188
+ }
@@ -0,0 +1,36 @@
1
+ // The values the rness mod keeps in `$.state` (spec 0029 §3.2). The
2
+ // snapshot is what `rness status --json` prints: `StatusSnapshot` in
3
+ // `src/commands/snapshot.ts`, read here as data.
4
+
5
+ export type RnessRow = {
6
+ id: string
7
+ title: string
8
+ status: string | null
9
+ path: string
10
+ }
11
+
12
+ export type RnessTab = { name: string; label: string; rows: RnessRow[] }
13
+
14
+ export type RnessSnapshot = {
15
+ version: string
16
+ workspace: string
17
+ scope: string | null
18
+ banner: string
19
+ statusLine: string
20
+ inProgress: string[]
21
+ notes: string[]
22
+ tabs: RnessTab[]
23
+ }
24
+
25
+ declare module 'claude-code' {
26
+ interface PluginState {
27
+ rness: {
28
+ /** The last snapshot; null while none could be read. */
29
+ snapshot: RnessSnapshot | null
30
+ /** The pane's tab, by its index in the snapshot. */
31
+ tab: number
32
+ /** Whether someone watches the session: no pane for `claude -p`. */
33
+ interactive: boolean
34
+ }
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rness/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Configuration-plane CLI for rness workspaces: resolve, validate and sync org-level context for AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,6 +23,10 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "scaffold",
26
+ "mod/.claude-plugin/plugin.json",
27
+ "mod/hooks/hooks.json",
28
+ "mod/hooks/register.tsx",
29
+ "mod/types/index.d.ts",
26
30
  "README.md",
27
31
  "LICENSE"
28
32
  ],
@@ -12,6 +12,11 @@
12
12
  - **Skill** (`skills/`): reusable agent skills, resolved per scope the same
13
13
  way.
14
14
 
15
+ ADRs, specifications and plans are named `NNNN-<slug>.md`: the next number
16
+ of their collection, which `rness doc new <collection>` gives, then a few
17
+ lowercase words of the title. `rness validate` reports a file named
18
+ otherwise, a dated name included.
19
+
15
20
  ## Metadata and status
16
21
 
17
22
  ADRs, specifications and plans start with YAML front matter before the first