@rness/cli 0.20.1 → 0.21.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 CHANGED
@@ -31,6 +31,7 @@ Inside a workspace, every `rness` delegates to the copy pinned in
31
31
  rness status [<tab>] [--json]
32
32
  rness pulse create [<collection>] [-y]
33
33
  rness pulse sync
34
+ rness pulse note [--plan <path>] [--kind approach|deviation|blocker|done] [<text>]
34
35
  rness validate
35
36
  rness mcp
36
37
 
@@ -244,7 +245,9 @@ workspace root's carry four Claude Code hooks, all run by the pinned copy:
244
245
  repository or nothing installed there, a `rness.json` rness refuses, a pin
245
246
  the installed copy does not match, the problems `rness validate` reports.
246
247
  A Claude Code too old for the plugin's mod gets one more line, once (see
247
- "The mod"). After a compaction, the context only.
248
+ "The mod"). When a board keeps a journal and the scope has a plan in
249
+ progress, the model gets one more line: how and when to post a note (see
250
+ "Pulse"). After a compaction, the context only.
248
251
  - **Before an edit** (`PreToolUse` on `Edit|Write`): refused, with the
249
252
  reason for the model, when it would change what `sync` generates — a
250
253
  line of the block of an `AGENTS.md` or `CLAUDE.md` (the reason names the
@@ -258,10 +261,13 @@ workspace root's carry four Claude Code hooks, all run by the pinned copy:
258
261
  - **After an edit** (`PostToolUse` on `Edit|Write`): when the file is under
259
262
  `.rness/`, its front-matter problems — or those of `rness.json` — go back
260
263
  to the model, which fixes them in the same turn. Any other edit costs one
261
- path check. Stale blocks are left to `sync`. With a pulse declared, the
262
- edit is then marked on the board, broken or not; see "Pulse".
263
- - **At session end** (`SessionEnd`): with a pulse declared, clears the marks
264
- this session set and syncs the board; see "Pulse". Without one, nothing.
264
+ path check. Stale blocks are left to `sync`. When a board declares
265
+ `mark` and holds the document, the edit is then marked on it, broken or
266
+ not; see "Pulse".
267
+ - **At session end** (`SessionEnd`): when a board declares `clear-marks`,
268
+ clears the marks this session set and syncs; when one declares
269
+ `journal-summary`, posts the session's summary; see "Pulse". Without
270
+ either, nothing.
265
271
 
266
272
  Each hook is one fixed `sh` line: it runs
267
273
  `<.rness>/node_modules/@rness/cli/dist/bin/rness.js hook <event>` when that
@@ -273,7 +279,8 @@ own hooks stay where they are.
273
279
 
274
280
  Claude Code runs hooks from a committed settings file without asking each
275
281
  developer, including in `claude -p`. These four read `.rness/`, write
276
- nothing in the workspace and run no install. Only with a pulse declared do
282
+ no file of the workspace and run no install; with a journal, the session
283
+ start records the clone's `HEAD` in its git directory. Only with a pulse declared do
277
284
  they reach the network, through a detached `rness` process that uses your
278
285
  login; when that process fails, it writes why to `pulse.json` in rness's
279
286
  configuration directory (`~/.config/rness/` by default), which the next
@@ -288,10 +295,70 @@ status, it received the problem. The `sh` line is not verified on Windows.
288
295
  rness pulse create marketing # a collection's own project (0.17.0)
289
296
  rness pulse sync # every declared project, as often as wanted
290
297
 
291
- A projection of rness's state on the organization's GitHub Projects: a
292
- project named **Agent Pulse**, where the team sees — and reads — every
293
- document of `.rness/`, and sees when an agent is at work on one. rness
294
- writes the board; if it is not in rness, it does not belong there.
298
+ A projection of rness's state on the organization's GitHub Projects,
299
+ each declared in `rness.json`: first **Agent Pulse**, where the team sees —
300
+ and reads — every document of `.rness/`, and sees when an agent is at work
301
+ on one. rness writes the boards; if it is not in rness, it does not belong
302
+ there.
303
+
304
+ - **Boards declared in `rness.json`** (0.21.0): `projects` maps a name to
305
+ a board, written whole — its `number`, `collections`, `colors`,
306
+ `fields`, `labels`, `views`, and optionally `title`, `description`,
307
+ `readme` and `updates` (files of `.rness/`). What `rness.json` says is
308
+ the board; what it does not declare, the board does not have, but the
309
+ engine's own: one issue of `.rness` per document, found by its `Path`,
310
+ its `Status`, the label `rness`. Nothing knows Agent Pulse by name: it
311
+ is a board like any other, made from the preset `agent-pulse`, and the
312
+ example a team copies for another.
313
+ - `collections`: `"all"` (ADR, specs and plans with the contract's
314
+ statuses, then every other directory `rness status` finds, but those
315
+ another board holds), or directories, each with `statuses` —
316
+ `"contract"`, `"found"` or a list in order — a `label` and an optional
317
+ `field`, a select of its statuses alone.
318
+ - `fields`: each a `type` (`text`, `date`, `select`, `number`) and a
319
+ `from`: a front-matter key, or what rness knows — `$collection`,
320
+ `$status`, `$agent` (`working` while a session marks the card),
321
+ `$session`, `$sessions`, `$id` — or a list, the first present winning.
322
+ `Path`, `Status` and GitHub's own fields cannot be declared.
323
+ - `views`: each a `name` and a `layout` (`board`, `table`, `roadmap`),
324
+ optionally `collection` and `filter`; a board's `columns`, a table's
325
+ `fields`, a roadmap's `date`.
326
+ - `colors`: option → `gray`, `blue`, `green`, `yellow`, `orange`, `red`,
327
+ `pink` or `purple`; an option not named takes its kind's colour (a
328
+ status its tone, a collection pink). A colour changed is set at the
329
+ next sync, every card keeping its value.
330
+ - A board that `rness.json` declares wrongly is refused alone, by its
331
+ full key (`"projects.marketing.views[2].date" must name a date
332
+ field`); the others sync. `validate` reports it, and also a collection
333
+ that is no directory of `.rness/`, one on two boards, a `readme` or
334
+ `updates` not there, a README that still declares (below). `readme`
335
+ and `updates` are paths within `.rness/`: a path out of it, or a file
336
+ that is a link leading out, is refused before anything is read.
337
+ - A view declared and missing is made; one whose filter or fields differ
338
+ is updated in place, keeping its id, sort and widths (GitHub returns a
339
+ filter as given and the fields in the project's order, read on
340
+ 2026-10-06); one whose layout or columns differ is made again. A view
341
+ rness made and no longer declared is deleted; one the team made is
342
+ left, and said once per clone (`note view Roadmap is not declared:
343
+ delete it on GitHub if unwanted`), as is a field no longer declared and
344
+ views out of the declared order (GitHub reorders none). A roadmap's
345
+ date is chosen in its settings: GitHub's API does not set it.
346
+ - **Presets and their revisions.** `pulse create` writes a board from a
347
+ preset, `agent-pulse` or `collection`, recording it (`"preset":
348
+ "agent-pulse/1"`). When a later rness ships a newer revision, `rness
349
+ sync` merges it into the board value by value: a value the team never
350
+ changed takes the new one, one it changed is kept, and said when the
351
+ revision changed it too (`kept projects.pulse.colors.Draft: kept orange,
352
+ agent-pulse/2 gives blue`). Removing `preset` takes a board off the
353
+ merge. An upgrade commits the result, for the team to read before
354
+ pushing.
355
+ - **From 0.20 and before.** A board declared by its number
356
+ (`"pulse": 4`) is read as its preset, and `rness sync` — which `rness
357
+ upgrade` runs — writes it whole; nothing changes on GitHub. A
358
+ collection's `README.md` that declared `description`, `statuses`,
359
+ `fields` or `labels` has them moved into its board and out of its front
360
+ matter (a value declared otherwise in both is refused, naming both).
361
+ Until then, `pulse sync` skips a board whose README still declares.
295
362
 
296
363
  - **Items**: one per document of `rness status`, an issue of
297
364
  `<org>/.rness` added to the project. GitHub shows it only to people who
@@ -381,8 +448,9 @@ Session → Working session`). A board per directory, named as
381
448
  that restricts OAuth apps must approve "Rness", as for its private
382
449
  repositories.
383
450
  - **`pulse create`** needs an `org` in `rness.json` (a blank workspace is
384
- refused) and no pulse yet. It creates the project and at once writes
385
- `"projects": { "pulse": <number> }` (and the `provider`) into `rness.json`,
451
+ refused) and no pulse yet. It creates the project and at once writes the
452
+ board whole, from the preset `agent-pulse`, into `projects.pulse` (and
453
+ the `provider`) in `rness.json`,
386
454
  then adds the fields, options and views, and runs a first sync; its
387
455
  `created` lines say what it added. If a step after the project fails, it
388
456
  exits 1 with the pulse declared, and `rness pulse sync` completes the
@@ -413,13 +481,17 @@ added to project`) may change a field of rness's items; the next sync
413
481
  writes it back.
414
482
  - **A collection's own project** (0.17.0): `rness pulse create <collection>`
415
483
  gives a directory of `.rness/` whose documents carry a status — a tab of
416
- `rness status` — a project of its own, named after it, declared as
417
- `"projects": { "pulse": 4, "marketing": 5 }`. Agent Pulse keeps every
484
+ `rness status` — a project of its own, named after it, written whole
485
+ from the preset `collection` into `projects.<collection>` (0.21.0;
486
+ before, `"projects": { "pulse": 4, "marketing": 5 }`). Agent Pulse keeps every
418
487
  other collection and the `Working` table; the collection's items leave
419
488
  it (`deleteProjectV2Item`, their issues untouched) once its project
420
489
  holds them. `pulse sync` syncs the collections' projects, then Agent
421
490
  Pulse. The project is written from the collection's files, one way:
422
- - `<collection>/README.md` is its README; its front matter may give
491
+ - `<collection>/README.md` is its README (`readme` in its board since
492
+ 0.21.0, where `description`, `statuses`, `fields` and `labels` are
493
+ declared too: what follows is how a README declared them before,
494
+ which `rness sync` moves); its front matter may give
423
495
  `description:` (the short description), `statuses:` (the columns, in
424
496
  order, each there even when empty), `fields:` (each a name, a `type` —
425
497
  `text`, `date`, `select`, `number` — and `from`, the front-matter key
@@ -438,9 +510,25 @@ added to project`) may change a field of rness's items; the next sync
438
510
  none of this reaches Agent Pulse: a workspace that declares no collection
439
511
  keeps its board as 0.16 made it.
440
512
 
441
- - **Hooks**, with `claude` in `agents` and a pulse declared: session start
442
- marks the `In progress` plans of the session's scope `Agent: working`, with
443
- the session; an edit of a document of `.rness/` marks it — one the agent has just written, with no item yet, gets its issue first
513
+ - **Hooks**, with `claude` in `agents`: what a session does to a board is
514
+ declared in its `hooks` (spec 0032, 0.21.0), each event — `session-start`,
515
+ `edit`, `session-end` — listing actions from a fixed catalogue:
516
+ `mark-in-progress` (the documents of the session's scope in the given
517
+ `collections` and `statuses`, `plans` and `In progress` by default),
518
+ `mark` (the document edited, on a board that holds it) and `clear-marks`
519
+ (the session's marks, then a sync). Marks are written through the
520
+ board's select from `$agent` and its text from `$session`, whatever their
521
+ names. A hook never runs a command `rness.json` names: the file is the
522
+ organization's and the hooks run on every developer's machine. The
523
+ preset `agent-pulse` declares all three, which is 0.20's behaviour;
524
+ removing `hooks` from a board turns its marking off, and a board without
525
+ them is only what `pulse sync` makes of the documents. For each event,
526
+ one detached `rness pulse run <event>` does every board's actions, in
527
+ the order of `projects`; a failure is recorded naming the board and the
528
+ action (`mark on Marketing failed: …`). `rness pulse mark`, which 0.20's
529
+ hooks started, stays, hidden, through 0.21.x. With Agent Pulse's preset:
530
+ session start marks the `In progress` plans of the session's scope
531
+ `Agent: working`, with the session; an edit of a document of `.rness/` marks it — one the agent has just written, with no item yet, gets its issue first
444
532
  through a sync, and so does one whose status the edit changed, so its
445
533
  card moves within seconds rather than at the session's end; an edit that
446
534
  leaves the status alone, or of any other file of `.rness/`, costs no sync;
@@ -457,6 +545,40 @@ added to project`) may change a field of rness's items; the next sync
457
545
  `the pulse needs the project scope: run rness login` or
458
546
  `cannot reach GitHub: …`. Two sessions marking one plan both write; the
459
547
  last wins.
548
+ - **Agent journal** (spec 0030, 0.21.0): how a plan was implemented, in
549
+ the agent's words. A board that holds `plans` declares
550
+ `{ "action": "journal", "to": "plan" | "repo", "limit": 5 }` at
551
+ `session-start`, and `journal-summary` at `session-end`. With a plan
552
+ `In progress` in the session's scope, the session start tells the model
553
+ to post a note — `rness pulse note`, or `rness_note` — when it chooses an
554
+ approach, deviates from the plan, is blocked, and when done: decisions
555
+ and their reasons, not steps. A note is a comment headed
556
+ `**Approach** · claude · 1a2b3c4d · \`feat/limits\` @ \`9f8e7d6\``
557
+ (`--kind`titles it; the branch and commit are those of the session's
558
+ repository), then the text, from the argument or stdin. With`"plan"`it goes to the plan's issue in`.rness`. With `"repo"`, to the plan's
559
+ implementation issue in the repository the session works in: labelled
560
+ `rness:plan`, found again by the last line of its body, made by the
561
+ first note and made a sub-issue of the plan's. The note prints where it
562
+ went (`acme/api#87`), for the pull request that completes the plan there
563
+ to carry `Closes acme/api#87`and an earlier one`Refs acme/api#87`.
564
+ GitHub closes the issue when a person merges that pull request into the
565
+ default branch; rness closes nothing. A repository with its issues off,
566
+ that refuses this login's write, or not under the organization on GitHub
567
+ sends the note to the plan's issue, its first line saying why, and the
568
+ next session start says so once. A session in `.rness/`or at the root
569
+ writes on the plan's issue. With several plans in progress,`--plan`names one. Each note holds 1 to 4,000 characters; a session posts at most`limit` per plan (`journal full for this session: put the rest in the
570
+ pull request`); a note that looks like it holds a credential (`ghp_`,
571
+ `gho_`, `github_pat_`, `AKIA…`, a private key, `xox…-`, `sk-…`) is
572
+ refused, never redacted. The counts stay in the `.rness` clone's git
573
+ directory (`rness/journal`), never pushed. At the session's end,
574
+ `journal-summary`posts`**Session summary** · claude · 1a2b3c4d ·
575
+ 47 min`, the branch and its commits since the session started (its
576
+ `HEAD`, recorded in the clone's git directory, `rness/sessions/<id>`)
577
+ and the branch's pull request: to each issue the notes went to, else to
578
+ the one plan in progress when there is a commit; with neither, nothing.
579
+ It counts toward no limit. Not verified against GitHub yet (2026-10-06):
580
+ a sub-issue across repositories, and `Closes` on a pull request merged
581
+ into the default branch.
460
582
  - **One issue per document**: before making an issue, `pulse sync` reads
461
583
  the open issues of `.rness` labelled `rness` (one request per 100, and
462
584
  only when it has one to make). One whose body starts with the document's
@@ -500,7 +622,9 @@ repository, the board).
500
622
  detected provider this version cannot talk to. The order of keys is
501
623
  `contract`, `provider`, `org`, `agents`, `projects`, `repos`, `scopes`.
502
624
  The former `"pulse": { "project": <number> }` still reads, as
503
- `"projects": { "pulse": <number> }`; both at once are refused.
625
+ `"projects": { "pulse": <number> }`; both at once are refused. Since
626
+ 0.21.0 a board in `projects` is written whole, on several lines; a
627
+ number still reads, as its preset, until `rness sync` writes it whole.
504
628
  - A provider _written_ in `rness.json` that this version cannot talk to
505
629
  (`gitlab`) is refused by `validate`, `create`, `login` and `pulse`:
506
630
  `provider "gitlab" is not supported by @rness/cli 0.17.1 (supported:
@@ -511,22 +635,26 @@ github)`. `add`, `sync` and the local commands never refuse. `null` is not a
511
635
 
512
636
  rness mcp # started by the agent over stdio, not by hand
513
637
 
514
- A local, read-only MCP server over the workspace's `.rness/`, for an agent to
515
- find what applies to the repository it works in and where a subject was
516
- decided:
638
+ A local MCP server over the workspace's `.rness/`, for an agent to find what
639
+ applies to the repository it works in and where a subject was decided. It
640
+ is read-only but for `rness_note`, listed only when a board keeps a journal
641
+ as the server starts (Claude Code starts it with the session):
517
642
 
518
- | Tool | Returns |
519
- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
520
- | `rness_context({ scope? })` | the scope of the working directory (or the one named) and what applies to it: standards, decisions, specifications, plans — id, status, title, path |
521
- | `rness_list({ collection, status? })` | every document of a collection, across scopes, optionally of one status |
522
- | `rness_read({ path })` | one file of `.rness/`, 256 KiB at most; nothing outside it |
523
- | `rness_search({ query, collection? })` | the documents that match, most matching first, with the matching lines |
643
+ | Tool | Returns |
644
+ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
645
+ | `rness_context({ scope? })` | the scope of the working directory (or the one named) and what applies to it: standards, decisions, specifications, plans — id, status, title, path |
646
+ | `rness_list({ collection, status? })` | every document of a collection, across scopes, optionally of one status |
647
+ | `rness_read({ path })` | one file of `.rness/`, 256 KiB at most; nothing outside it |
648
+ | `rness_search({ query, collection? })` | the documents that match, most matching first, with the matching lines |
649
+ | `rness_note({ text, session, kind?, plan? })` | a note of the agent's journal posted to GitHub, as `rness pulse note` posts it; the issue it went to (see "Pulse") |
524
650
 
525
651
  - MCP over stdio, one message per line: the `2026-07-28` revision and the
526
652
  earlier ones that open with `initialize` (`2025-11-25` back to
527
653
  `2024-11-05`). No MCP SDK, no added dependency.
528
654
  - `.rness/` is read again on each call, so an edit shows at once. Nothing is
529
- written.
655
+ written, but by `rness_note`, which posts with your login. It takes the
656
+ session as an argument (`claude · 1a2b3c4d`, as the session start spells
657
+ it): the server's environment names none.
530
658
  - With `claude` in `agents`, `sync` registers it in each clone:
531
659
  `.mcp.json` gets `mcpServers.rness`, which runs the pinned copy
532
660
  (`node ../../.rness/node_modules/@rness/cli/dist/bin/rness.js mcp`). A
@@ -695,6 +823,44 @@ per repository, the files to commit there.
695
823
  Exit codes: 0 success, 1 failure, 2 usage — or a refusal without a TTY.
696
824
  `RNESS_DEBUG=1` adds stack traces; `RNESS_NO_DELEGATE=1` skips the delegation.
697
825
 
826
+ ## 0.21.0 — boards declared in rness.json; what a session does to a board, declared; the agent's journal
827
+
828
+ - The agent journal (spec 0030, see "Pulse"): a board declares `journal`
829
+ at `session-start` and `journal-summary` at `session-end`. The agent
830
+ posts notes on the plan it implements — `rness pulse note`, or
831
+ `rness_note` of `rness mcp` — to the plan's issue in `.rness`, or to an
832
+ implementation issue in the repository it works in, a sub-issue of the
833
+ plan's, which the pull request closes. The session's end posts a summary:
834
+ its commits, its pull request, its duration. The preset `agent-pulse`
835
+ declares neither: nothing changes until a board does.
836
+ - Boards are declared in `rness.json` (spec 0031, see "Pulse"): each one's
837
+ collections, statuses, colours, fields, labels and views, written whole.
838
+ Agent Pulse is the preset `agent-pulse`, no longer a special case of the
839
+ code. `pulse create` writes the board it makes; `rness sync` (which
840
+ `rness upgrade` runs) writes a board declared by its number whole, moves
841
+ a collection's README declarations into its board, and merges a newer
842
+ revision of a preset value by value. Views are updated in place where
843
+ GitHub allows; a view rness made and no longer declared is deleted, the
844
+ team's are said once. Nothing changes on GitHub for a board 0.20 made.
845
+ For the package's API: `Projects` is `Record<string, number |
846
+ BoardDeclaration>`; `BoardDeclaration` and `parseBoard` are exported.
847
+ - A board's `readme` and `updates` stay within `.rness/`: a path out of
848
+ it, or a link leading out, is refused before anything is read.
849
+ - What a session does to a board is declared in its `hooks` (spec 0032):
850
+ `mark-in-progress`, `mark` and `clear-marks`, from a fixed catalogue; a
851
+ hook never runs a command `rness.json` names. The preset `agent-pulse`
852
+ declares today's three, so nothing changes on GitHub. The hooks start
853
+ `rness pulse run <event>`; `rness pulse mark` stays, hidden, through
854
+ 0.21.x, for sessions started before an upgrade.
855
+ - `upgrade` run by a workspace's pinned copy installed with pnpm no longer
856
+ ends on `mod directory not found next to the @rness/cli package`. After
857
+ its commit it lists the files to commit in the clones, and that list read
858
+ the plugin's mod from the folder of the copy running it, which the install
859
+ had just removed. The list now takes the paths alone. The upgrade itself
860
+ was complete: 0.20.0 to 0.20.1 printed the error after its commit. An
861
+ upgrade from 0.20.1 still prints it, since the copy that runs it is the
862
+ old one; `pnpm dlx @rness/cli upgrade` runs the new one.
863
+
698
864
  ## 0.20.1 — the workspace's line in the prompt footer, without a warning sign; the mod never installs the pin
699
865
 
700
866
  - The workspace's line in Claude Code moves from the pinned status line to
package/dist/bin/rness.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  OWN_COMMANDS,
4
- VERSION,
5
4
  catchUp,
6
5
  findDelegate,
7
- reportError,
8
6
  run
9
- } from "../chunk-2AFAQO5B.js";
7
+ } from "../chunk-T5PINUCP.js";
8
+ import {
9
+ VERSION,
10
+ reportError
11
+ } from "../chunk-D3D2WCGW.js";
10
12
 
11
13
  // src/bin/rness.ts
12
14
  import { pathToFileURL } from "url";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/bin/rness.ts","../../src/core/node-version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { pathToFileURL } from 'node:url'\n\nimport { run } from '../cli.ts'\nimport { OWN_COMMANDS, catchUp } from '../core/catch-up.ts'\nimport { findDelegate } from '../core/delegate.ts'\nimport { MIN_NODE_LABEL, isSupportedNode } from '../core/node-version.ts'\nimport { reportError } from '../report.ts'\nimport { VERSION } from '../version.ts'\n\n// `rness context | head`: once the reader has its lines it closes the pipe and\n// every further write emits EPIPE on stdout. Nothing is left to deliver, so end\n// quietly with success. This is the one process.exit() in the code base — it\n// runs only when there is no output left to drain (see main() below).\nprocess.stdout.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'EPIPE') process.exit(0)\n throw e\n})\n\n// Set process.exitCode rather than calling process.exit(): process.exit()\n// terminates before Node drains an async stdout pipe, truncating piped output\n// at the ~64 KiB pipe buffer (breaks `rness context --json | jq`, `$(...)`).\nasync function main(): Promise<number> {\n if (!isSupportedNode(process.versions.node)) {\n process.stderr.write(\n `rness needs Node ${MIN_NODE_LABEL} or newer (running ${process.version})\\n`\n )\n return 1\n }\n const argv = process.argv.slice(2)\n const skipDelegation =\n OWN_COMMANDS.includes(argv[0] ?? '') ||\n process.env['RNESS_NO_DELEGATE'] === '1'\n // A pin that moved is installed first, and the fresh copy runs the command\n // as its own process (spec 0008 §4).\n const handedOff = await catchUp(argv)\n if (handedOff !== null) return handedOff\n const delegate = skipDelegation\n ? null\n : await findDelegate(process.cwd(), VERSION)\n if (delegate !== null) {\n const broken = (reason: string): Error =>\n new Error(\n `@rness/cli ${delegate.version} pinned in ${delegate.root}/.rness is not installed correctly (${reason}); reinstall in .rness/`\n )\n if (process.env['RNESS_DEBUG'] === '1') {\n process.stderr.write(\n `rness: delegating to @rness/cli ${delegate.version} (${delegate.entry})\\n`\n )\n }\n let mod: Record<string, unknown>\n try {\n mod = (await import(pathToFileURL(delegate.entry).href)) as Record<\n string,\n unknown\n >\n } catch (e) {\n throw broken(e instanceof Error ? e.message : String(e))\n }\n if (typeof mod['run'] !== 'function')\n throw broken('dist/index.js does not export run')\n const delegateRun = mod['run'] as (argv: string[]) => Promise<unknown>\n const code = await delegateRun(argv)\n return typeof code === 'number' ? code : 1\n }\n return run(argv)\n}\n\nmain()\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n process.exitCode = reportError(e)\n })\n","/** The runtime floor of the published CLI (ADR 0010): Node 22.17, the line where `fs.glob` is stable. */\nexport const MIN_NODE = { major: 22, minor: 17 } as const\nexport const MIN_NODE_LABEL = `${MIN_NODE.major}.${MIN_NODE.minor}`\n\nfunction component(version: string, index: number): number {\n const n = Number.parseInt(version.split('.')[index] ?? '', 10)\n return Number.isNaN(n) ? -1 : n\n}\n\nexport function nodeMajor(version: string): number {\n return Math.max(component(version, 0), 0)\n}\n\n/** True from `MIN_NODE` on: a newer major, or the same major from its minor. */\nexport function isSupportedNode(version: string): boolean {\n const major = component(version, 0)\n const minor = component(version, 1)\n if (major < 0 || minor < 0) return false\n if (major !== MIN_NODE.major) return major > MIN_NODE.major\n return minor >= MIN_NODE.minor\n}\n"],"mappings":";;;;;;;;;;;AACA,SAAS,qBAAqB;;;ACAvB,IAAM,WAAW,EAAE,OAAO,IAAI,OAAO,GAAG;AACxC,IAAM,iBAAiB,GAAG,SAAS,KAAK,IAAI,SAAS,KAAK;AAEjE,SAAS,UAAU,SAAiB,OAAuB;AACzD,QAAM,IAAI,OAAO,SAAS,QAAQ,MAAM,GAAG,EAAE,KAAK,KAAK,IAAI,EAAE;AAC7D,SAAO,OAAO,MAAM,CAAC,IAAI,KAAK;AAChC;AAOO,SAAS,gBAAgB,SAA0B;AACxD,QAAM,QAAQ,UAAU,SAAS,CAAC;AAClC,QAAM,QAAQ,UAAU,SAAS,CAAC;AAClC,MAAI,QAAQ,KAAK,QAAQ,EAAG,QAAO;AACnC,MAAI,UAAU,SAAS,MAAO,QAAO,QAAQ,SAAS;AACtD,SAAO,SAAS,SAAS;AAC3B;;;ADNA,QAAQ,OAAO,GAAG,SAAS,CAAC,MAA6B;AACvD,MAAI,EAAE,SAAS,QAAS,SAAQ,KAAK,CAAC;AACtC,QAAM;AACR,CAAC;AAKD,eAAe,OAAwB;AACrC,MAAI,CAAC,gBAAgB,QAAQ,SAAS,IAAI,GAAG;AAC3C,YAAQ,OAAO;AAAA,MACb,oBAAoB,cAAc,sBAAsB,QAAQ,OAAO;AAAA;AAAA,IACzE;AACA,WAAO;AAAA,EACT;AACA,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,QAAM,iBACJ,aAAa,SAAS,KAAK,CAAC,KAAK,EAAE,KACnC,QAAQ,IAAI,mBAAmB,MAAM;AAGvC,QAAM,YAAY,MAAM,QAAQ,IAAI;AACpC,MAAI,cAAc,KAAM,QAAO;AAC/B,QAAM,WAAW,iBACb,OACA,MAAM,aAAa,QAAQ,IAAI,GAAG,OAAO;AAC7C,MAAI,aAAa,MAAM;AACrB,UAAM,SAAS,CAAC,WACd,IAAI;AAAA,MACF,cAAc,SAAS,OAAO,cAAc,SAAS,IAAI,uCAAuC,MAAM;AAAA,IACxG;AACF,QAAI,QAAQ,IAAI,aAAa,MAAM,KAAK;AACtC,cAAQ,OAAO;AAAA,QACb,mCAAmC,SAAS,OAAO,KAAK,SAAS,KAAK;AAAA;AAAA,MACxE;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,YAAO,MAAM,OAAO,cAAc,SAAS,KAAK,EAAE;AAAA,IAIpD,SAAS,GAAG;AACV,YAAM,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACzD;AACA,QAAI,OAAO,IAAI,KAAK,MAAM;AACxB,YAAM,OAAO,mCAAmC;AAClD,UAAM,cAAc,IAAI,KAAK;AAC7B,UAAM,OAAO,MAAM,YAAY,IAAI;AACnC,WAAO,OAAO,SAAS,WAAW,OAAO;AAAA,EAC3C;AACA,SAAO,IAAI,IAAI;AACjB;AAEA,KAAK,EACF,KAAK,CAAC,SAAS;AACd,UAAQ,WAAW;AACrB,CAAC,EACA,MAAM,CAAC,MAAe;AACrB,UAAQ,WAAW,YAAY,CAAC;AAClC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../src/bin/rness.ts","../../src/core/node-version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { pathToFileURL } from 'node:url'\n\nimport { run } from '../cli.ts'\nimport { OWN_COMMANDS, catchUp } from '../core/catch-up.ts'\nimport { findDelegate } from '../core/delegate.ts'\nimport { MIN_NODE_LABEL, isSupportedNode } from '../core/node-version.ts'\nimport { reportError } from '../report.ts'\nimport { VERSION } from '../version.ts'\n\n// `rness context | head`: once the reader has its lines it closes the pipe and\n// every further write emits EPIPE on stdout. Nothing is left to deliver, so end\n// quietly with success. This is the one process.exit() in the code base — it\n// runs only when there is no output left to drain (see main() below).\nprocess.stdout.on('error', (e: NodeJS.ErrnoException) => {\n if (e.code === 'EPIPE') process.exit(0)\n throw e\n})\n\n// Set process.exitCode rather than calling process.exit(): process.exit()\n// terminates before Node drains an async stdout pipe, truncating piped output\n// at the ~64 KiB pipe buffer (breaks `rness context --json | jq`, `$(...)`).\nasync function main(): Promise<number> {\n if (!isSupportedNode(process.versions.node)) {\n process.stderr.write(\n `rness needs Node ${MIN_NODE_LABEL} or newer (running ${process.version})\\n`\n )\n return 1\n }\n const argv = process.argv.slice(2)\n const skipDelegation =\n OWN_COMMANDS.includes(argv[0] ?? '') ||\n process.env['RNESS_NO_DELEGATE'] === '1'\n // A pin that moved is installed first, and the fresh copy runs the command\n // as its own process (spec 0008 §4).\n const handedOff = await catchUp(argv)\n if (handedOff !== null) return handedOff\n const delegate = skipDelegation\n ? null\n : await findDelegate(process.cwd(), VERSION)\n if (delegate !== null) {\n const broken = (reason: string): Error =>\n new Error(\n `@rness/cli ${delegate.version} pinned in ${delegate.root}/.rness is not installed correctly (${reason}); reinstall in .rness/`\n )\n if (process.env['RNESS_DEBUG'] === '1') {\n process.stderr.write(\n `rness: delegating to @rness/cli ${delegate.version} (${delegate.entry})\\n`\n )\n }\n let mod: Record<string, unknown>\n try {\n mod = (await import(pathToFileURL(delegate.entry).href)) as Record<\n string,\n unknown\n >\n } catch (e) {\n throw broken(e instanceof Error ? e.message : String(e))\n }\n if (typeof mod['run'] !== 'function')\n throw broken('dist/index.js does not export run')\n const delegateRun = mod['run'] as (argv: string[]) => Promise<unknown>\n const code = await delegateRun(argv)\n return typeof code === 'number' ? code : 1\n }\n return run(argv)\n}\n\nmain()\n .then((code) => {\n process.exitCode = code\n })\n .catch((e: unknown) => {\n process.exitCode = reportError(e)\n })\n","/** The runtime floor of the published CLI (ADR 0010): Node 22.17, the line where `fs.glob` is stable. */\nexport const MIN_NODE = { major: 22, minor: 17 } as const\nexport const MIN_NODE_LABEL = `${MIN_NODE.major}.${MIN_NODE.minor}`\n\nfunction component(version: string, index: number): number {\n const n = Number.parseInt(version.split('.')[index] ?? '', 10)\n return Number.isNaN(n) ? -1 : n\n}\n\nexport function nodeMajor(version: string): number {\n return Math.max(component(version, 0), 0)\n}\n\n/** True from `MIN_NODE` on: a newer major, or the same major from its minor. */\nexport function isSupportedNode(version: string): boolean {\n const major = component(version, 0)\n const minor = component(version, 1)\n if (major < 0 || minor < 0) return false\n if (major !== MIN_NODE.major) return major > MIN_NODE.major\n return minor >= MIN_NODE.minor\n}\n"],"mappings":";;;;;;;;;;;;;AACA,SAAS,qBAAqB;;;ACAvB,IAAM,WAAW,EAAE,OAAO,IAAI,OAAO,GAAG;AACxC,IAAM,iBAAiB,GAAG,SAAS,KAAK,IAAI,SAAS,KAAK;AAEjE,SAAS,UAAU,SAAiB,OAAuB;AACzD,QAAM,IAAI,OAAO,SAAS,QAAQ,MAAM,GAAG,EAAE,KAAK,KAAK,IAAI,EAAE;AAC7D,SAAO,OAAO,MAAM,CAAC,IAAI,KAAK;AAChC;AAOO,SAAS,gBAAgB,SAA0B;AACxD,QAAM,QAAQ,UAAU,SAAS,CAAC;AAClC,QAAM,QAAQ,UAAU,SAAS,CAAC;AAClC,MAAI,QAAQ,KAAK,QAAQ,EAAG,QAAO;AACnC,MAAI,UAAU,SAAS,MAAO,QAAO,QAAQ,SAAS;AACtD,SAAO,SAAS,SAAS;AAC3B;;;ADNA,QAAQ,OAAO,GAAG,SAAS,CAAC,MAA6B;AACvD,MAAI,EAAE,SAAS,QAAS,SAAQ,KAAK,CAAC;AACtC,QAAM;AACR,CAAC;AAKD,eAAe,OAAwB;AACrC,MAAI,CAAC,gBAAgB,QAAQ,SAAS,IAAI,GAAG;AAC3C,YAAQ,OAAO;AAAA,MACb,oBAAoB,cAAc,sBAAsB,QAAQ,OAAO;AAAA;AAAA,IACzE;AACA,WAAO;AAAA,EACT;AACA,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,QAAM,iBACJ,aAAa,SAAS,KAAK,CAAC,KAAK,EAAE,KACnC,QAAQ,IAAI,mBAAmB,MAAM;AAGvC,QAAM,YAAY,MAAM,QAAQ,IAAI;AACpC,MAAI,cAAc,KAAM,QAAO;AAC/B,QAAM,WAAW,iBACb,OACA,MAAM,aAAa,QAAQ,IAAI,GAAG,OAAO;AAC7C,MAAI,aAAa,MAAM;AACrB,UAAM,SAAS,CAAC,WACd,IAAI;AAAA,MACF,cAAc,SAAS,OAAO,cAAc,SAAS,IAAI,uCAAuC,MAAM;AAAA,IACxG;AACF,QAAI,QAAQ,IAAI,aAAa,MAAM,KAAK;AACtC,cAAQ,OAAO;AAAA,QACb,mCAAmC,SAAS,OAAO,KAAK,SAAS,KAAK;AAAA;AAAA,MACxE;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,YAAO,MAAM,OAAO,cAAc,SAAS,KAAK,EAAE;AAAA,IAIpD,SAAS,GAAG;AACV,YAAM,OAAO,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,IACzD;AACA,QAAI,OAAO,IAAI,KAAK,MAAM;AACxB,YAAM,OAAO,mCAAmC;AAClD,UAAM,cAAc,IAAI,KAAK;AAC7B,UAAM,OAAO,MAAM,YAAY,IAAI;AACnC,WAAO,OAAO,SAAS,WAAW,OAAO;AAAA,EAC3C;AACA,SAAO,IAAI,IAAI;AACjB;AAEA,KAAK,EACF,KAAK,CAAC,SAAS;AACd,UAAQ,WAAW;AACrB,CAAC,EACA,MAAM,CAAC,MAAe;AACrB,UAAQ,WAAW,YAAY,CAAC;AAClC,CAAC;","names":[]}
@@ -0,0 +1,263 @@
1
+ import {
2
+ JournalRefused,
3
+ blobUrl,
4
+ branchOf,
5
+ cloneHolding,
6
+ context,
7
+ declaredBoards,
8
+ fromGithub,
9
+ headOf,
10
+ issueUrl,
11
+ notePosted,
12
+ notesPosted,
13
+ parseFrontMatter,
14
+ plansInProgress,
15
+ recordNotice,
16
+ resolveScope,
17
+ statusTabs
18
+ } from "./chunk-D3D2WCGW.js";
19
+
20
+ // src/pulse/note.ts
21
+ import { relative, resolve, sep } from "path";
22
+ var KINDS = ["approach", "deviation", "blocker", "done"];
23
+ var TITLE = {
24
+ approach: "Approach",
25
+ deviation: "Deviation",
26
+ blocker: "Blocker",
27
+ done: "Done"
28
+ };
29
+ var MAX = 4e3;
30
+ var SECRETS = [
31
+ /ghp_/,
32
+ /gho_/,
33
+ /github_pat_/,
34
+ /AKIA[0-9A-Z]{16}/,
35
+ /-----BEGIN [A-Z ]*PRIVATE KEY/,
36
+ /xox[abpr]-/,
37
+ /sk-[A-Za-z0-9]{20,}/
38
+ ];
39
+ var NoteRefused = class extends Error {
40
+ };
41
+ function sessionFrom(given, env) {
42
+ const raw = given ?? env["CLAUDE_CODE_SESSION_ID"] ?? "";
43
+ if (raw.trim() === "") return null;
44
+ return raw.includes(" \xB7 ") ? raw : `claude \xB7 ${raw.slice(0, 8)}`;
45
+ }
46
+ function checked(input) {
47
+ const length = input.text.trim().length;
48
+ if (length < 1 || length > MAX)
49
+ throw new NoteRefused(
50
+ `a note holds 1 to ${MAX.toLocaleString("en")} characters (this one ${length})`
51
+ );
52
+ if (SECRETS.some((s) => s.test(input.text)))
53
+ throw new NoteRefused("the note looks like it holds a credential");
54
+ const kind = KINDS.find((k) => k === (input.kind ?? "approach"));
55
+ if (kind === void 0)
56
+ throw new NoteRefused(
57
+ `--kind is one of ${KINDS.slice(0, -1).join(", ")} or ${KINDS.at(-1)}`
58
+ );
59
+ const session = sessionFrom(input.session, input.env ?? process.env);
60
+ if (session === null)
61
+ throw new NoteRefused(
62
+ "no session: write it from a Claude Code session, or pass --session"
63
+ );
64
+ return { kind, session };
65
+ }
66
+ function journalOf(declared) {
67
+ for (const d of declared)
68
+ for (const a of d.declaration.hooks["session-start"] ?? [])
69
+ if (a.action === "journal") return { board: d, journal: a };
70
+ return null;
71
+ }
72
+ function githubRepo(url) {
73
+ const m = /^(?:git@github\.com:|ssh:\/\/git@github\.com\/|https:\/\/github\.com\/)([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(
74
+ url.trim()
75
+ );
76
+ return m === null || m[1] === void 0 || m[2] === void 0 ? null : { owner: m[1], name: m[2] };
77
+ }
78
+ async function planOf(c, cwd, given) {
79
+ const tabs = await statusTabs(c.rnessDir);
80
+ const plans = tabs.find((t) => t.name === "plans")?.rows ?? [];
81
+ if (given !== void 0) {
82
+ const path = given.replace(/^\.rness\//, "");
83
+ if (!plans.some((r) => r.path === path))
84
+ throw new NoteRefused(`${given} is no plan of .rness`);
85
+ return path;
86
+ }
87
+ const scope = resolveScope(
88
+ c.manifest,
89
+ relative(c.root, resolve(cwd)).split(sep).join("/")
90
+ );
91
+ const candidates = await plansInProgress(
92
+ { root: c.root, rnessDir: c.rnessDir },
93
+ c.manifest,
94
+ scope
95
+ );
96
+ if (candidates.length === 0)
97
+ throw new NoteRefused(
98
+ `no plan in progress in scope ${scope ?? "global"}: set one In progress first`
99
+ );
100
+ if (candidates.length > 1)
101
+ throw new NoteRefused(
102
+ `several plans in progress: pass --plan, one of ${candidates.join(", ")}`
103
+ );
104
+ return candidates[0] ?? "";
105
+ }
106
+ async function planIssue(c, board, plan) {
107
+ const items = await fromGithub(
108
+ c.provider.items(board.board, { bodies: false })
109
+ );
110
+ const number = items.find((i) => i.path === plan)?.issue?.number;
111
+ const found = number === void 0 ? null : await fromGithub(c.provider.issueOf(c.org, ".rness", number));
112
+ if (found === null)
113
+ throw new NoteRefused(`${plan} has no issue yet: rness pulse sync`);
114
+ return {
115
+ id: found.id,
116
+ number: found.number,
117
+ repository: `${c.org}/.rness`
118
+ };
119
+ }
120
+ var refusedWrite = (e) => e instanceof Error && /not accessible|forbidden|permission|must have push access/i.test(e.message);
121
+ async function implementationBody(c, plan, planNumber) {
122
+ const lines = [
123
+ `Plan [\`${plan}\`](${blobUrl(c.org, plan)}) \xB7 [plan issue](${issueUrl(c.org, planNumber)})`
124
+ ];
125
+ try {
126
+ const { readFile } = await import("fs/promises");
127
+ const front = parseFrontMatter(
128
+ await readFile(resolve(c.rnessDir, ...plan.split("/")), "utf8")
129
+ ) ?? {};
130
+ const spec = /\(\.\.\/(specs\/[^)\s]+\.md)\)/.exec(
131
+ String(front["spec"] ?? "")
132
+ );
133
+ if (spec?.[1] !== void 0)
134
+ lines.push(`Specification [\`${spec[1]}\`](${blobUrl(c.org, spec[1])})`);
135
+ } catch {
136
+ }
137
+ return `${lines.join("\n")}
138
+
139
+ The agent's notes on this implementation follow as comments. The pull request that completes it carries \`Closes\` with this issue's reference.`;
140
+ }
141
+ async function postNote(input) {
142
+ const { kind, session } = checked(input);
143
+ const c = await context({
144
+ cwd: input.cwd,
145
+ ...input.githubApi === void 0 ? {} : { githubApi: input.githubApi }
146
+ });
147
+ const declared = await declaredBoards(c);
148
+ const kept = journalOf(declared);
149
+ if (kept === null)
150
+ throw new NoteRefused(
151
+ 'no board keeps a journal: add { "action": "journal", "to": "plan" } to its session-start hooks'
152
+ );
153
+ const plan = await planOf(c, input.cwd, input.plan);
154
+ if (await notesPosted(c.rnessDir, session, plan) >= kept.journal.limit)
155
+ throw new NoteRefused(
156
+ "journal full for this session: put the rest in the pull request"
157
+ );
158
+ const parent = await planIssue(c, kept.board, plan);
159
+ const clone = cloneHolding(c.root, c.manifest.repos, input.cwd);
160
+ let target = parent;
161
+ let why = null;
162
+ if (kept.journal.to === "repo" && clone !== null) {
163
+ const repo = githubRepo(clone.url);
164
+ if (repo === null || repo.owner.toLowerCase() !== c.org.toLowerCase())
165
+ why = `${clone.name} is not on the workspace's provider`;
166
+ else
167
+ try {
168
+ const there = await fromGithub(
169
+ c.provider.journalIssue(repo.owner, repo.name, plan)
170
+ );
171
+ if (there !== null) target = there;
172
+ else {
173
+ const row = (await statusTabs(c.rnessDir)).find((t) => t.name === "plans")?.rows.find((r) => r.path === plan);
174
+ target = await fromGithub(
175
+ c.provider.createJournalIssue(repo.owner, repo.name, plan, {
176
+ title: `Implement ${row === void 0 ? plan : `${row.id} \u2014 ${row.title}`}`,
177
+ body: await implementationBody(c, plan, parent.number)
178
+ })
179
+ );
180
+ try {
181
+ await fromGithub(c.provider.addSubIssue(parent.id, target.id));
182
+ } catch {
183
+ await fromGithub(
184
+ c.provider.comment(
185
+ parent.id,
186
+ `Implementation: ${target.repository}#${target.number}`
187
+ )
188
+ );
189
+ }
190
+ }
191
+ } catch (e) {
192
+ if (e instanceof JournalRefused)
193
+ why = e.why === "issues" ? `${repo.owner}/${repo.name} takes no issues` : `${repo.owner}/${repo.name} is not on GitHub, or this login cannot see it`;
194
+ else if (refusedWrite(e))
195
+ why = `${repo.owner}/${repo.name} refuses this login's write`;
196
+ else throw e;
197
+ }
198
+ }
199
+ if (why !== null)
200
+ await recordNotice(`journal \u2014 ${why}, notes go to the plan's issue`);
201
+ const where = clone?.dir ?? input.cwd;
202
+ const [branch, head] = await Promise.all([branchOf(where), headOf(where)]);
203
+ const id = session.split(" \xB7 ").pop() ?? session;
204
+ const header = [
205
+ `**${TITLE[kind]}**`,
206
+ session,
207
+ ...branch === null || head === null ? [] : [`\`${branch}\` @ \`${head.slice(0, 7)}\``]
208
+ ].join(" \xB7 ");
209
+ const body = [
210
+ ...why === null ? [] : [`_${why}: this note goes to the plan's issue._`, ""],
211
+ header,
212
+ "",
213
+ input.text.trim(),
214
+ "",
215
+ `<!-- rness note ${id} -->`
216
+ ].join("\n");
217
+ await fromGithub(c.provider.comment(target.id, body));
218
+ const reference = `${target.repository}#${target.number}`;
219
+ await notePosted(c.rnessDir, session, plan, { id: target.id, reference });
220
+ return {
221
+ reference,
222
+ to: target === parent ? "plan" : "repo"
223
+ };
224
+ }
225
+ async function pulseNoteCommand(opts) {
226
+ try {
227
+ const text = opts.text === void 0 || opts.text === "-" ? await (opts.stdin ?? readStdin)() : opts.text;
228
+ const posted = await postNote({
229
+ cwd: opts.cwd ?? process.cwd(),
230
+ text,
231
+ ...opts.kind === void 0 ? {} : { kind: opts.kind },
232
+ ...opts.plan === void 0 ? {} : { plan: opts.plan },
233
+ ...opts.session === void 0 ? {} : { session: opts.session },
234
+ ...opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi }
235
+ });
236
+ process.stdout.write(`${posted.reference}
237
+ `);
238
+ return 0;
239
+ } catch (e) {
240
+ process.stderr.write(
241
+ `rness: ${e instanceof Error ? e.message : String(e)}
242
+ `
243
+ );
244
+ return 1;
245
+ }
246
+ }
247
+ async function readStdin() {
248
+ const chunks = [];
249
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
250
+ return Buffer.concat(chunks).toString("utf8");
251
+ }
252
+
253
+ export {
254
+ KINDS,
255
+ NoteRefused,
256
+ sessionFrom,
257
+ journalOf,
258
+ githubRepo,
259
+ planIssue,
260
+ postNote,
261
+ pulseNoteCommand
262
+ };
263
+ //# sourceMappingURL=chunk-4BM4FUWO.js.map