@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/README.md +120 -10
- package/dist/bin/rness.js +1 -1
- package/dist/{chunk-VGBS7HDY.js → chunk-U5SBNWVD.js} +760 -356
- package/dist/chunk-U5SBNWVD.js.map +1 -0
- package/dist/index.js +1 -1
- package/mod/.claude-plugin/plugin.json +21 -0
- package/mod/hooks/hooks.json +1 -0
- package/mod/hooks/register.tsx +188 -0
- package/mod/types/index.d.ts +36 -0
- package/package.json +5 -1
- package/scaffold/CONVENTIONS.md +5 -0
- package/dist/chunk-VGBS7HDY.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -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.
|
|
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
|
],
|
package/scaffold/CONVENTIONS.md
CHANGED
|
@@ -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
|